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,399 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module Agents
5
+ # Result wrapper for image pipeline operations
6
+ #
7
+ # Provides access to individual step results, aggregated costs,
8
+ # timing information, and the final processed image.
9
+ #
10
+ # @example Accessing pipeline results
11
+ # result = ProductPipeline.call(prompt: "Laptop photo")
12
+ # result.success? # => true
13
+ # result.final_image # => The final processed image URL/data
14
+ # result.total_cost # => Combined cost of all steps
15
+ # result.steps # => Array of step results
16
+ #
17
+ # @example Accessing specific step results
18
+ # result.step(:generate) # => ImageGenerationResult
19
+ # result.step(:upscale) # => ImageUpscaleResult
20
+ # result.step(:analyze) # => ImageAnalysisResult
21
+ # result.analysis # => Shortcut to analyzer step result
22
+ #
23
+ class ImagePipelineResult
24
+ attr_reader :step_results, :started_at, :completed_at, :tenant_id,
25
+ :pipeline_class, :context, :error_class, :error_message
26
+
27
+ # Initialize a new pipeline result
28
+ #
29
+ # @param step_results [Array<Hash>] Array of step result hashes
30
+ # @param started_at [Time] When pipeline started
31
+ # @param completed_at [Time] When pipeline completed
32
+ # @param tenant_id [String, nil] Tenant identifier
33
+ # @param pipeline_class [String] Name of the pipeline class
34
+ # @param context [Hash] Pipeline context
35
+ # @param error_class [String, nil] Error class name if failed
36
+ # @param error_message [String, nil] Error message if failed
37
+ def initialize(step_results:, started_at:, completed_at:, tenant_id:,
38
+ pipeline_class:, context:, error_class: nil, error_message: nil)
39
+ @step_results = step_results
40
+ @started_at = started_at
41
+ @completed_at = completed_at
42
+ @tenant_id = tenant_id
43
+ @pipeline_class = pipeline_class
44
+ @context = context
45
+ @error_class = error_class
46
+ @error_message = error_message
47
+ end
48
+
49
+ # Status helpers
50
+
51
+ # Check if pipeline completed successfully
52
+ #
53
+ # @return [Boolean] true if all steps succeeded
54
+ def success?
55
+ return false if error_class
56
+
57
+ step_results.all? { |s| s[:result]&.success? }
58
+ end
59
+
60
+ # Check if pipeline had any errors
61
+ #
62
+ # @return [Boolean] true if any step failed
63
+ def error?
64
+ !success?
65
+ end
66
+
67
+ # Check if pipeline completed (with or without errors)
68
+ #
69
+ # @return [Boolean] true if pipeline finished
70
+ def completed?
71
+ !error_class || step_results.any?
72
+ end
73
+
74
+ # Check if pipeline was partially successful
75
+ #
76
+ # @return [Boolean] true if some steps succeeded but not all
77
+ def partial?
78
+ return false if error_class && step_results.empty?
79
+
80
+ has_success = step_results.any? { |s| s[:result]&.success? }
81
+ has_error = step_results.any? { |s| s[:result]&.error? }
82
+ has_success && has_error
83
+ end
84
+
85
+ # Step access
86
+
87
+ # Get all steps as array
88
+ #
89
+ # @return [Array<Hash>] Array of step result hashes
90
+ def steps
91
+ step_results
92
+ end
93
+
94
+ # Get a specific step result by name
95
+ #
96
+ # @param name [Symbol] Step name
97
+ # @return [Object, nil] The step result or nil
98
+ def step(name)
99
+ step_data = step_results.find { |s| s[:name] == name }
100
+ step_data&.dig(:result)
101
+ end
102
+
103
+ alias [] step
104
+
105
+ # Get step names
106
+ #
107
+ # @return [Array<Symbol>] Array of step names
108
+ def step_names
109
+ step_results.map { |s| s[:name] }
110
+ end
111
+
112
+ # Count helpers
113
+
114
+ # Total number of steps in pipeline
115
+ #
116
+ # @return [Integer] Step count
117
+ def step_count
118
+ step_results.size
119
+ end
120
+
121
+ # Number of successful steps
122
+ #
123
+ # @return [Integer] Successful step count
124
+ def successful_step_count
125
+ step_results.count { |s| s[:result]&.success? }
126
+ end
127
+
128
+ # Number of failed steps
129
+ #
130
+ # @return [Integer] Failed step count
131
+ def failed_step_count
132
+ step_results.count { |s| s[:result]&.error? }
133
+ end
134
+
135
+ # Image access
136
+
137
+ # Get the final image from the last successful image-producing step
138
+ #
139
+ # @return [String, nil] URL or data of final image
140
+ def final_image
141
+ # Find last successful step that produces an image (not analyzer)
142
+ image_step = step_results.reverse.find do |s|
143
+ s[:type] != :analyzer && s[:result]&.success?
144
+ end
145
+ return nil unless image_step
146
+
147
+ result = image_step[:result]
148
+ result.url || result.data
149
+ end
150
+
151
+ # Get the final image URL
152
+ #
153
+ # @return [String, nil] URL of final image
154
+ def url
155
+ image_step = step_results.reverse.find do |s|
156
+ s[:type] != :analyzer && s[:result]&.success? && s[:result].respond_to?(:url)
157
+ end
158
+ image_step&.dig(:result)&.url
159
+ end
160
+
161
+ # Get the final image data
162
+ #
163
+ # @return [String, nil] Base64 data of final image
164
+ def data
165
+ image_step = step_results.reverse.find do |s|
166
+ s[:type] != :analyzer && s[:result]&.success? && s[:result].respond_to?(:data)
167
+ end
168
+ image_step&.dig(:result)&.data
169
+ end
170
+
171
+ # Check if final image is base64 encoded
172
+ #
173
+ # @return [Boolean] true if base64
174
+ def base64?
175
+ image_step = step_results.reverse.find do |s|
176
+ s[:type] != :analyzer && s[:result]&.success?
177
+ end
178
+ image_step&.dig(:result)&.base64? || false
179
+ end
180
+
181
+ # Get the final image as binary blob
182
+ #
183
+ # @return [String, nil] Binary image data
184
+ def to_blob
185
+ image_step = step_results.reverse.find do |s|
186
+ s[:type] != :analyzer && s[:result]&.success? && s[:result].respond_to?(:to_blob)
187
+ end
188
+ image_step&.dig(:result)&.to_blob
189
+ end
190
+
191
+ # Shortcut accessors for common step types
192
+
193
+ # Get the analysis result if an analyzer step was run
194
+ #
195
+ # @return [ImageAnalysisResult, nil] Analysis result
196
+ def analysis
197
+ analyzer_step = step_results.find { |s| s[:type] == :analyzer }
198
+ analyzer_step&.dig(:result)
199
+ end
200
+
201
+ # Get the generation result if a generator step was run
202
+ #
203
+ # @return [ImageGenerationResult, nil] Generation result
204
+ def generation
205
+ generator_step = step_results.find { |s| s[:type] == :generator }
206
+ generator_step&.dig(:result)
207
+ end
208
+
209
+ # Get the upscale result if an upscaler step was run
210
+ #
211
+ # @return [ImageUpscaleResult, nil] Upscale result
212
+ def upscale
213
+ upscaler_step = step_results.find { |s| s[:type] == :upscaler }
214
+ upscaler_step&.dig(:result)
215
+ end
216
+
217
+ # Get the transform result if a transformer step was run
218
+ #
219
+ # @return [ImageTransformResult, nil] Transform result
220
+ def transform
221
+ transformer_step = step_results.find { |s| s[:type] == :transformer }
222
+ transformer_step&.dig(:result)
223
+ end
224
+
225
+ # Get the background removal result if a remover step was run
226
+ #
227
+ # @return [BackgroundRemovalResult, nil] Removal result
228
+ def background_removal
229
+ remover_step = step_results.find { |s| s[:type] == :remover }
230
+ remover_step&.dig(:result)
231
+ end
232
+
233
+ # Timing
234
+
235
+ # Pipeline duration in milliseconds
236
+ #
237
+ # @return [Integer] Duration in ms
238
+ def duration_ms
239
+ return 0 unless started_at && completed_at
240
+ ((completed_at - started_at) * 1000).round
241
+ end
242
+
243
+ # Cost
244
+
245
+ # Total cost of all pipeline steps
246
+ #
247
+ # @return [Float] Combined cost
248
+ def total_cost
249
+ step_results.sum { |s| s[:result]&.total_cost || 0 }
250
+ end
251
+
252
+ # Get the primary model ID (from first step)
253
+ #
254
+ # @return [String, nil] Model ID
255
+ def primary_model_id
256
+ first_result = step_results.first&.dig(:result)
257
+ first_result&.model_id
258
+ end
259
+
260
+ # File operations
261
+
262
+ # Save the final image to a file
263
+ #
264
+ # @param path [String] File path
265
+ # @return [void]
266
+ def save(path)
267
+ image_step = step_results.reverse.find do |s|
268
+ s[:type] != :analyzer && s[:result]&.success? && s[:result].respond_to?(:save)
269
+ end
270
+ raise "No image to save" unless image_step
271
+
272
+ image_step[:result].save(path)
273
+ end
274
+
275
+ # Save all intermediate images
276
+ #
277
+ # @param directory [String] Directory path
278
+ # @param prefix [String] Filename prefix
279
+ # @return [void]
280
+ def save_all(directory, prefix: "step")
281
+ step_results.each_with_index do |step, idx|
282
+ next if step[:type] == :analyzer
283
+ next unless step[:result]&.success? && step[:result].respond_to?(:save)
284
+
285
+ filename = "#{prefix}_#{idx + 1}_#{step[:name]}.png"
286
+ step[:result].save(File.join(directory, filename))
287
+ end
288
+ end
289
+
290
+ # Serialization
291
+
292
+ # Convert to hash
293
+ #
294
+ # @return [Hash] Hash representation
295
+ def to_h
296
+ {
297
+ success: success?,
298
+ partial: partial?,
299
+ step_count: step_count,
300
+ successful_steps: successful_step_count,
301
+ failed_steps: failed_step_count,
302
+ steps: step_results.map do |s|
303
+ {
304
+ name: s[:name],
305
+ type: s[:type],
306
+ success: s[:result]&.success?,
307
+ cost: s[:result]&.total_cost
308
+ }
309
+ end,
310
+ final_image_url: url,
311
+ total_cost: total_cost,
312
+ duration_ms: duration_ms,
313
+ started_at: started_at&.iso8601,
314
+ completed_at: completed_at&.iso8601,
315
+ tenant_id: tenant_id,
316
+ pipeline_class: pipeline_class,
317
+ error_class: error_class,
318
+ error_message: error_message
319
+ }
320
+ end
321
+
322
+ # Caching
323
+
324
+ # Convert to cacheable format
325
+ #
326
+ # @return [Hash] Cacheable hash
327
+ def to_cache
328
+ {
329
+ step_results: step_results.map do |s|
330
+ {
331
+ name: s[:name],
332
+ type: s[:type],
333
+ cached_result: s[:result]&.respond_to?(:to_cache) ? s[:result].to_cache : nil
334
+ }
335
+ end,
336
+ total_cost: total_cost,
337
+ cached_at: Time.current.iso8601
338
+ }
339
+ end
340
+
341
+ # Restore from cache
342
+ #
343
+ # @param data [Hash] Cached data
344
+ # @return [CachedImagePipelineResult]
345
+ def self.from_cache(data)
346
+ CachedImagePipelineResult.new(data)
347
+ end
348
+ end
349
+
350
+ # Lightweight result for cached pipelines
351
+ class CachedImagePipelineResult
352
+ attr_reader :step_results, :total_cost, :cached_at
353
+
354
+ def initialize(data)
355
+ @step_results = data[:step_results] || []
356
+ @total_cost = data[:total_cost]
357
+ @cached_at = data[:cached_at]
358
+ end
359
+
360
+ def success?
361
+ step_results.any?
362
+ end
363
+
364
+ def error?
365
+ !success?
366
+ end
367
+
368
+ def cached?
369
+ true
370
+ end
371
+
372
+ def step_count
373
+ step_results.size
374
+ end
375
+
376
+ def step(name)
377
+ step_data = step_results.find { |s| s[:name] == name }
378
+ step_data&.dig(:cached_result)
379
+ end
380
+
381
+ alias [] step
382
+
383
+ def final_image
384
+ # Find last non-analyzer step
385
+ image_step = step_results.reverse.find { |s| s[:type] != :analyzer }
386
+ return nil unless image_step
387
+
388
+ cached = image_step[:cached_result]
389
+ return nil unless cached
390
+
391
+ cached[:urls]&.first || cached[:url] || cached[:datas]&.first || cached[:data]
392
+ end
393
+
394
+ def url
395
+ final_image if final_image.is_a?(String) && final_image.start_with?("http")
396
+ end
397
+ end
398
+ end
399
+ end
@@ -0,0 +1,251 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module Agents
5
+ # Result wrapper for image transformation operations
6
+ #
7
+ # Provides a consistent interface for accessing transformed images,
8
+ # metadata, timing, and cost information.
9
+ #
10
+ # @example Accessing transformed image
11
+ # result = ImageTransformer.call(
12
+ # image: "photo.jpg",
13
+ # prompt: "Convert to watercolor style"
14
+ # )
15
+ # result.url # => "https://..."
16
+ # result.strength # => 0.75
17
+ # result.success? # => true
18
+ #
19
+ class ImageTransformResult
20
+ attr_reader :images, :source_image, :prompt, :model_id, :size, :strength,
21
+ :started_at, :completed_at, :tenant_id, :transformer_class,
22
+ :error_class, :error_message
23
+
24
+ # Initialize a new result
25
+ #
26
+ # @param images [Array<Object>] Array of transformed image objects
27
+ # @param source_image [String] The original source image
28
+ # @param prompt [String] The transformation prompt
29
+ # @param model_id [String] Model used for transformation
30
+ # @param size [String] Image size
31
+ # @param strength [Float] Transformation strength used
32
+ # @param started_at [Time] When transformation started
33
+ # @param completed_at [Time] When transformation completed
34
+ # @param tenant_id [String, nil] Tenant identifier
35
+ # @param transformer_class [String] Name of the transformer class
36
+ # @param error_class [String, nil] Error class name if failed
37
+ # @param error_message [String, nil] Error message if failed
38
+ def initialize(images:, source_image:, prompt:, model_id:, size:, strength:,
39
+ started_at:, completed_at:, tenant_id:, transformer_class:,
40
+ error_class: nil, error_message: nil)
41
+ @images = images
42
+ @source_image = source_image
43
+ @prompt = prompt
44
+ @model_id = model_id
45
+ @size = size
46
+ @strength = strength
47
+ @started_at = started_at
48
+ @completed_at = completed_at
49
+ @tenant_id = tenant_id
50
+ @transformer_class = transformer_class
51
+ @error_class = error_class
52
+ @error_message = error_message
53
+ end
54
+
55
+ # Status helpers
56
+
57
+ def success?
58
+ error_class.nil? && images.any?
59
+ end
60
+
61
+ def error?
62
+ !success?
63
+ end
64
+
65
+ def single?
66
+ count == 1
67
+ end
68
+
69
+ def batch?
70
+ count > 1
71
+ end
72
+
73
+ # Image access
74
+
75
+ def image
76
+ images.first
77
+ end
78
+
79
+ def url
80
+ image&.url
81
+ end
82
+
83
+ def urls
84
+ images.map(&:url).compact
85
+ end
86
+
87
+ def data
88
+ image&.data
89
+ end
90
+
91
+ def datas
92
+ images.map(&:data).compact
93
+ end
94
+
95
+ def base64?
96
+ image&.base64? || false
97
+ end
98
+
99
+ def mime_type
100
+ image&.mime_type
101
+ end
102
+
103
+ def revised_prompt
104
+ image&.revised_prompt
105
+ end
106
+
107
+ # Count
108
+
109
+ def count
110
+ images.size
111
+ end
112
+
113
+ # Timing
114
+
115
+ def duration_ms
116
+ return 0 unless started_at && completed_at
117
+ ((completed_at - started_at) * 1000).round
118
+ end
119
+
120
+ # Cost estimation
121
+
122
+ def total_cost
123
+ return 0 if error?
124
+
125
+ ImageGenerator::Pricing.calculate_cost(
126
+ model_id: model_id,
127
+ size: size,
128
+ count: count
129
+ )
130
+ end
131
+
132
+ def input_tokens
133
+ (prompt.length / 4.0).ceil
134
+ end
135
+
136
+ # File operations
137
+
138
+ def save(path)
139
+ raise "No image to save" unless image
140
+ image.save(path)
141
+ end
142
+
143
+ def save_all(directory, prefix: "transformed")
144
+ images.each_with_index do |img, idx|
145
+ filename = "#{prefix}_#{idx + 1}.png"
146
+ img.save(File.join(directory, filename))
147
+ end
148
+ end
149
+
150
+ def to_blob
151
+ image&.to_blob
152
+ end
153
+
154
+ def blobs
155
+ images.map(&:to_blob)
156
+ end
157
+
158
+ # Serialization
159
+
160
+ def to_h
161
+ {
162
+ success: success?,
163
+ count: count,
164
+ urls: urls,
165
+ base64: base64?,
166
+ mime_type: mime_type,
167
+ source_image: source_image,
168
+ prompt: prompt,
169
+ model_id: model_id,
170
+ size: size,
171
+ strength: strength,
172
+ total_cost: total_cost,
173
+ duration_ms: duration_ms,
174
+ started_at: started_at&.iso8601,
175
+ completed_at: completed_at&.iso8601,
176
+ tenant_id: tenant_id,
177
+ transformer_class: transformer_class,
178
+ error_class: error_class,
179
+ error_message: error_message
180
+ }
181
+ end
182
+
183
+ # Caching
184
+
185
+ def to_cache
186
+ {
187
+ urls: urls,
188
+ datas: datas,
189
+ mime_type: mime_type,
190
+ model_id: model_id,
191
+ total_cost: total_cost,
192
+ cached_at: Time.current.iso8601
193
+ }
194
+ end
195
+
196
+ def self.from_cache(data)
197
+ CachedImageTransformResult.new(data)
198
+ end
199
+ end
200
+
201
+ # Lightweight result for cached transformations
202
+ class CachedImageTransformResult
203
+ attr_reader :urls, :datas, :mime_type, :model_id, :total_cost, :cached_at
204
+
205
+ def initialize(data)
206
+ @urls = data[:urls] || []
207
+ @datas = data[:datas] || []
208
+ @mime_type = data[:mime_type]
209
+ @model_id = data[:model_id]
210
+ @total_cost = data[:total_cost]
211
+ @cached_at = data[:cached_at]
212
+ end
213
+
214
+ def success?
215
+ urls.any? || datas.any?
216
+ end
217
+
218
+ def error?
219
+ !success?
220
+ end
221
+
222
+ def cached?
223
+ true
224
+ end
225
+
226
+ def url
227
+ urls.first
228
+ end
229
+
230
+ def data
231
+ datas.first
232
+ end
233
+
234
+ def base64?
235
+ datas.any?
236
+ end
237
+
238
+ def count
239
+ [urls.size, datas.size].max
240
+ end
241
+
242
+ def single?
243
+ count == 1
244
+ end
245
+
246
+ def batch?
247
+ count > 1
248
+ end
249
+ end
250
+ end
251
+ end