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,250 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module Agents
5
+ # Result wrapper for image editing operations
6
+ #
7
+ # Provides a consistent interface for accessing edited images,
8
+ # metadata, timing, and cost information.
9
+ #
10
+ # @example Accessing edited image
11
+ # result = ImageEditor.call(
12
+ # image: "photo.png",
13
+ # mask: "mask.png",
14
+ # prompt: "Replace background"
15
+ # )
16
+ # result.url # => "https://..."
17
+ # result.success? # => true
18
+ #
19
+ class ImageEditResult
20
+ attr_reader :images, :source_image, :mask, :prompt, :model_id, :size,
21
+ :started_at, :completed_at, :tenant_id, :editor_class,
22
+ :error_class, :error_message
23
+
24
+ # Initialize a new result
25
+ #
26
+ # @param images [Array<Object>] Array of edited image objects
27
+ # @param source_image [String] The original source image
28
+ # @param mask [String] The mask image used
29
+ # @param prompt [String] The edit prompt
30
+ # @param model_id [String] Model used for editing
31
+ # @param size [String] Image size
32
+ # @param started_at [Time] When editing started
33
+ # @param completed_at [Time] When editing completed
34
+ # @param tenant_id [String, nil] Tenant identifier
35
+ # @param editor_class [String] Name of the editor 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:, mask:, prompt:, model_id:, size:,
39
+ started_at:, completed_at:, tenant_id:, editor_class:,
40
+ error_class: nil, error_message: nil)
41
+ @images = images
42
+ @source_image = source_image
43
+ @mask = mask
44
+ @prompt = prompt
45
+ @model_id = model_id
46
+ @size = size
47
+ @started_at = started_at
48
+ @completed_at = completed_at
49
+ @tenant_id = tenant_id
50
+ @editor_class = editor_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: "edited")
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
+ total_cost: total_cost,
172
+ duration_ms: duration_ms,
173
+ started_at: started_at&.iso8601,
174
+ completed_at: completed_at&.iso8601,
175
+ tenant_id: tenant_id,
176
+ editor_class: editor_class,
177
+ error_class: error_class,
178
+ error_message: error_message
179
+ }
180
+ end
181
+
182
+ # Caching
183
+
184
+ def to_cache
185
+ {
186
+ urls: urls,
187
+ datas: datas,
188
+ mime_type: mime_type,
189
+ model_id: model_id,
190
+ total_cost: total_cost,
191
+ cached_at: Time.current.iso8601
192
+ }
193
+ end
194
+
195
+ def self.from_cache(data)
196
+ CachedImageEditResult.new(data)
197
+ end
198
+ end
199
+
200
+ # Lightweight result for cached edits
201
+ class CachedImageEditResult
202
+ attr_reader :urls, :datas, :mime_type, :model_id, :total_cost, :cached_at
203
+
204
+ def initialize(data)
205
+ @urls = data[:urls] || []
206
+ @datas = data[:datas] || []
207
+ @mime_type = data[:mime_type]
208
+ @model_id = data[:model_id]
209
+ @total_cost = data[:total_cost]
210
+ @cached_at = data[:cached_at]
211
+ end
212
+
213
+ def success?
214
+ urls.any? || datas.any?
215
+ end
216
+
217
+ def error?
218
+ !success?
219
+ end
220
+
221
+ def cached?
222
+ true
223
+ end
224
+
225
+ def url
226
+ urls.first
227
+ end
228
+
229
+ def data
230
+ datas.first
231
+ end
232
+
233
+ def base64?
234
+ datas.any?
235
+ end
236
+
237
+ def count
238
+ [urls.size, datas.size].max
239
+ end
240
+
241
+ def single?
242
+ count == 1
243
+ end
244
+
245
+ def batch?
246
+ count > 1
247
+ end
248
+ end
249
+ end
250
+ end
@@ -0,0 +1,346 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module Agents
5
+ # Result wrapper for image generation operations
6
+ #
7
+ # Provides a consistent interface for accessing generated images,
8
+ # metadata, timing, and cost information.
9
+ #
10
+ # @example Accessing a single image
11
+ # result = ImageGenerator.call(prompt: "A sunset")
12
+ # result.url # => "https://..."
13
+ # result.success? # => true
14
+ # result.save("sunset.png")
15
+ #
16
+ # @example Accessing multiple images
17
+ # result = ImageGenerator.call(prompt: "Logos", count: 4)
18
+ # result.urls # => ["https://...", ...]
19
+ # result.count # => 4
20
+ # result.save_all("./logos")
21
+ #
22
+ class ImageGenerationResult
23
+ attr_reader :images, :prompt, :model_id, :size, :quality, :style,
24
+ :started_at, :completed_at, :tenant_id, :generator_class,
25
+ :error_class, :error_message
26
+
27
+ # Initialize a new result
28
+ #
29
+ # @param images [Array<Object>] Array of image objects from RubyLLM
30
+ # @param prompt [String] The original prompt
31
+ # @param model_id [String] Model used for generation
32
+ # @param size [String] Image size
33
+ # @param quality [String] Quality setting
34
+ # @param style [String] Style setting
35
+ # @param started_at [Time] When generation started
36
+ # @param completed_at [Time] When generation completed
37
+ # @param tenant_id [String, nil] Tenant identifier
38
+ # @param generator_class [String] Name of the generator class
39
+ # @param error_class [String, nil] Error class name if failed
40
+ # @param error_message [String, nil] Error message if failed
41
+ def initialize(images:, prompt:, model_id:, size:, quality:, style:,
42
+ started_at:, completed_at:, tenant_id:, generator_class:,
43
+ error_class: nil, error_message: nil)
44
+ @images = images
45
+ @prompt = prompt
46
+ @model_id = model_id
47
+ @size = size
48
+ @quality = quality
49
+ @style = style
50
+ @started_at = started_at
51
+ @completed_at = completed_at
52
+ @tenant_id = tenant_id
53
+ @generator_class = generator_class
54
+ @error_class = error_class
55
+ @error_message = error_message
56
+ end
57
+
58
+ # Status helpers
59
+
60
+ # Check if generation was successful
61
+ #
62
+ # @return [Boolean] true if successful
63
+ def success?
64
+ error_class.nil? && images.any?
65
+ end
66
+
67
+ # Check if generation failed
68
+ #
69
+ # @return [Boolean] true if failed
70
+ def error?
71
+ !success?
72
+ end
73
+
74
+ # Check if this was a single image request
75
+ #
76
+ # @return [Boolean] true if single image
77
+ def single?
78
+ count == 1
79
+ end
80
+
81
+ # Check if this was a batch request
82
+ #
83
+ # @return [Boolean] true if multiple images
84
+ def batch?
85
+ count > 1
86
+ end
87
+
88
+ # Image access
89
+
90
+ # Get the first/only image
91
+ #
92
+ # @return [Object, nil] The first image object
93
+ def image
94
+ images.first
95
+ end
96
+
97
+ # Get the URL of the first image
98
+ #
99
+ # @return [String, nil] The image URL
100
+ def url
101
+ image&.url
102
+ end
103
+
104
+ # Get all image URLs
105
+ #
106
+ # @return [Array<String>] Array of URLs
107
+ def urls
108
+ images.map(&:url).compact
109
+ end
110
+
111
+ # Get the base64 data of the first image
112
+ #
113
+ # @return [String, nil] Base64 encoded image data
114
+ def data
115
+ image&.data
116
+ end
117
+
118
+ # Get all base64 data
119
+ #
120
+ # @return [Array<String>] Array of base64 data
121
+ def datas
122
+ images.map(&:data).compact
123
+ end
124
+
125
+ # Check if the image is base64 encoded
126
+ #
127
+ # @return [Boolean] true if base64
128
+ def base64?
129
+ image&.base64? || false
130
+ end
131
+
132
+ # Get the MIME type
133
+ #
134
+ # @return [String, nil] MIME type
135
+ def mime_type
136
+ image&.mime_type
137
+ end
138
+
139
+ # Get the revised prompt (if model modified it)
140
+ #
141
+ # @return [String, nil] The revised prompt
142
+ def revised_prompt
143
+ image&.revised_prompt
144
+ end
145
+
146
+ # Get all revised prompts
147
+ #
148
+ # @return [Array<String>] Array of revised prompts
149
+ def revised_prompts
150
+ images.map(&:revised_prompt).compact
151
+ end
152
+
153
+ # Count
154
+
155
+ # Get the number of generated images
156
+ #
157
+ # @return [Integer] Image count
158
+ def count
159
+ images.size
160
+ end
161
+
162
+ # Timing
163
+
164
+ # Get the generation duration in milliseconds
165
+ #
166
+ # @return [Integer] Duration in ms
167
+ def duration_ms
168
+ return 0 unless started_at && completed_at
169
+ ((completed_at - started_at) * 1000).round
170
+ end
171
+
172
+ # Cost estimation
173
+
174
+ # Get the total cost for this generation
175
+ #
176
+ # Uses dynamic pricing from the Pricing module
177
+ #
178
+ # @return [Float] Total cost in USD
179
+ def total_cost
180
+ return 0 if error?
181
+
182
+ ImageGenerator::Pricing.calculate_cost(
183
+ model_id: model_id,
184
+ size: size,
185
+ quality: quality,
186
+ count: count
187
+ )
188
+ end
189
+
190
+ # Estimate input tokens from prompt
191
+ #
192
+ # @return [Integer] Approximate token count
193
+ def input_tokens
194
+ # Approximate token count for prompt
195
+ (prompt.length / 4.0).ceil
196
+ end
197
+
198
+ # File operations
199
+
200
+ # Save the first image to a file
201
+ #
202
+ # @param path [String] File path to save to
203
+ # @raise [RuntimeError] If no image to save
204
+ def save(path)
205
+ raise "No image to save" unless image
206
+ image.save(path)
207
+ end
208
+
209
+ # Save all images to a directory
210
+ #
211
+ # @param directory [String] Directory path
212
+ # @param prefix [String] Filename prefix
213
+ def save_all(directory, prefix: "image")
214
+ images.each_with_index do |img, idx|
215
+ filename = "#{prefix}_#{idx + 1}.png"
216
+ img.save(File.join(directory, filename))
217
+ end
218
+ end
219
+
220
+ # Get the first image as binary data
221
+ #
222
+ # @return [String, nil] Binary image data
223
+ def to_blob
224
+ image&.to_blob
225
+ end
226
+
227
+ # Get all images as binary data
228
+ #
229
+ # @return [Array<String>] Array of binary data
230
+ def blobs
231
+ images.map(&:to_blob)
232
+ end
233
+
234
+ # Serialization
235
+
236
+ # Convert to hash
237
+ #
238
+ # @return [Hash] Hash representation
239
+ def to_h
240
+ {
241
+ success: success?,
242
+ count: count,
243
+ urls: urls,
244
+ base64: base64?,
245
+ mime_type: mime_type,
246
+ prompt: prompt,
247
+ revised_prompts: revised_prompts,
248
+ model_id: model_id,
249
+ size: size,
250
+ quality: quality,
251
+ style: style,
252
+ total_cost: total_cost,
253
+ input_tokens: input_tokens,
254
+ duration_ms: duration_ms,
255
+ started_at: started_at&.iso8601,
256
+ completed_at: completed_at&.iso8601,
257
+ tenant_id: tenant_id,
258
+ generator_class: generator_class,
259
+ error_class: error_class,
260
+ error_message: error_message
261
+ }
262
+ end
263
+
264
+ # Caching
265
+
266
+ # Convert to cacheable format
267
+ #
268
+ # @return [Hash] Cache-friendly hash
269
+ def to_cache
270
+ {
271
+ urls: urls,
272
+ datas: datas,
273
+ mime_type: mime_type,
274
+ revised_prompts: revised_prompts,
275
+ model_id: model_id,
276
+ total_cost: total_cost,
277
+ cached_at: Time.current.iso8601
278
+ }
279
+ end
280
+
281
+ # Create a result from cached data
282
+ #
283
+ # @param data [Hash] Cached data
284
+ # @return [CachedImageGenerationResult] The cached result
285
+ def self.from_cache(data)
286
+ CachedImageGenerationResult.new(data)
287
+ end
288
+ end
289
+
290
+ # Lightweight result for cached images
291
+ #
292
+ # Provides a subset of ImageGenerationResult functionality
293
+ # for results loaded from cache.
294
+ #
295
+ class CachedImageGenerationResult
296
+ attr_reader :urls, :datas, :mime_type, :revised_prompts, :model_id,
297
+ :total_cost, :cached_at
298
+
299
+ def initialize(data)
300
+ @urls = data[:urls] || []
301
+ @datas = data[:datas] || []
302
+ @mime_type = data[:mime_type]
303
+ @revised_prompts = data[:revised_prompts] || []
304
+ @model_id = data[:model_id]
305
+ @total_cost = data[:total_cost]
306
+ @cached_at = data[:cached_at]
307
+ end
308
+
309
+ def success?
310
+ urls.any? || datas.any?
311
+ end
312
+
313
+ def error?
314
+ !success?
315
+ end
316
+
317
+ def cached?
318
+ true
319
+ end
320
+
321
+ def url
322
+ urls.first
323
+ end
324
+
325
+ def data
326
+ datas.first
327
+ end
328
+
329
+ def base64?
330
+ datas.any?
331
+ end
332
+
333
+ def count
334
+ [urls.size, datas.size].max
335
+ end
336
+
337
+ def single?
338
+ count == 1
339
+ end
340
+
341
+ def batch?
342
+ count > 1
343
+ end
344
+ end
345
+ end
346
+ end