ruby_llm-agents 0.5.0 → 1.0.0.beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (190) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +189 -31
  3. data/app/controllers/ruby_llm/agents/agents_controller.rb +136 -16
  4. data/app/controllers/ruby_llm/agents/dashboard_controller.rb +29 -9
  5. data/app/controllers/ruby_llm/agents/workflows_controller.rb +355 -0
  6. data/app/helpers/ruby_llm/agents/application_helper.rb +25 -0
  7. data/app/models/ruby_llm/agents/execution.rb +3 -0
  8. data/app/models/ruby_llm/agents/tenant_budget.rb +58 -15
  9. data/app/services/ruby_llm/agents/agent_registry.rb +51 -12
  10. data/app/views/layouts/ruby_llm/agents/application.html.erb +2 -29
  11. data/app/views/ruby_llm/agents/agents/_agent.html.erb +13 -1
  12. data/app/views/ruby_llm/agents/agents/_config_agent.html.erb +235 -0
  13. data/app/views/ruby_llm/agents/agents/_config_embedder.html.erb +70 -0
  14. data/app/views/ruby_llm/agents/agents/_config_image_generator.html.erb +152 -0
  15. data/app/views/ruby_llm/agents/agents/_config_moderator.html.erb +63 -0
  16. data/app/views/ruby_llm/agents/agents/_config_speaker.html.erb +108 -0
  17. data/app/views/ruby_llm/agents/agents/_config_transcriber.html.erb +91 -0
  18. data/app/views/ruby_llm/agents/agents/_workflow.html.erb +1 -1
  19. data/app/views/ruby_llm/agents/agents/index.html.erb +74 -9
  20. data/app/views/ruby_llm/agents/agents/show.html.erb +18 -378
  21. data/app/views/ruby_llm/agents/dashboard/_agent_comparison.html.erb +269 -15
  22. data/app/views/ruby_llm/agents/executions/show.html.erb +16 -0
  23. data/app/views/ruby_llm/agents/shared/_agent_type_badge.html.erb +93 -0
  24. data/app/views/ruby_llm/agents/workflows/_step_performance.html.erb +236 -0
  25. data/app/views/ruby_llm/agents/workflows/_structure_parallel.html.erb +76 -0
  26. data/app/views/ruby_llm/agents/workflows/_structure_pipeline.html.erb +74 -0
  27. data/app/views/ruby_llm/agents/workflows/_structure_router.html.erb +108 -0
  28. data/app/views/ruby_llm/agents/workflows/show.html.erb +442 -0
  29. data/config/routes.rb +1 -0
  30. data/lib/generators/ruby_llm_agents/agent_generator.rb +56 -7
  31. data/lib/generators/ruby_llm_agents/background_remover_generator.rb +110 -0
  32. data/lib/generators/ruby_llm_agents/embedder_generator.rb +107 -0
  33. data/lib/generators/ruby_llm_agents/image_analyzer_generator.rb +115 -0
  34. data/lib/generators/ruby_llm_agents/image_editor_generator.rb +108 -0
  35. data/lib/generators/ruby_llm_agents/image_generator_generator.rb +116 -0
  36. data/lib/generators/ruby_llm_agents/image_pipeline_generator.rb +178 -0
  37. data/lib/generators/ruby_llm_agents/image_transformer_generator.rb +109 -0
  38. data/lib/generators/ruby_llm_agents/image_upscaler_generator.rb +103 -0
  39. data/lib/generators/ruby_llm_agents/image_variator_generator.rb +102 -0
  40. data/lib/generators/ruby_llm_agents/install_generator.rb +76 -4
  41. data/lib/generators/ruby_llm_agents/restructure_generator.rb +292 -0
  42. data/lib/generators/ruby_llm_agents/speaker_generator.rb +121 -0
  43. data/lib/generators/ruby_llm_agents/templates/add_execution_type_migration.rb.tt +8 -0
  44. data/lib/generators/ruby_llm_agents/templates/agent.rb.tt +99 -84
  45. data/lib/generators/ruby_llm_agents/templates/application_agent.rb.tt +42 -40
  46. data/lib/generators/ruby_llm_agents/templates/application_background_remover.rb.tt +26 -0
  47. data/lib/generators/ruby_llm_agents/templates/application_embedder.rb.tt +50 -0
  48. data/lib/generators/ruby_llm_agents/templates/application_image_analyzer.rb.tt +26 -0
  49. data/lib/generators/ruby_llm_agents/templates/application_image_editor.rb.tt +20 -0
  50. data/lib/generators/ruby_llm_agents/templates/application_image_generator.rb.tt +38 -0
  51. data/lib/generators/ruby_llm_agents/templates/application_image_pipeline.rb.tt +139 -0
  52. data/lib/generators/ruby_llm_agents/templates/application_image_transformer.rb.tt +21 -0
  53. data/lib/generators/ruby_llm_agents/templates/application_image_upscaler.rb.tt +20 -0
  54. data/lib/generators/ruby_llm_agents/templates/application_image_variator.rb.tt +20 -0
  55. data/lib/generators/ruby_llm_agents/templates/application_speaker.rb.tt +49 -0
  56. data/lib/generators/ruby_llm_agents/templates/application_transcriber.rb.tt +53 -0
  57. data/lib/generators/ruby_llm_agents/templates/background_remover.rb.tt +44 -0
  58. data/lib/generators/ruby_llm_agents/templates/embedder.rb.tt +41 -0
  59. data/lib/generators/ruby_llm_agents/templates/image_analyzer.rb.tt +45 -0
  60. data/lib/generators/ruby_llm_agents/templates/image_editor.rb.tt +35 -0
  61. data/lib/generators/ruby_llm_agents/templates/image_generator.rb.tt +47 -0
  62. data/lib/generators/ruby_llm_agents/templates/image_pipeline.rb.tt +50 -0
  63. data/lib/generators/ruby_llm_agents/templates/image_transformer.rb.tt +44 -0
  64. data/lib/generators/ruby_llm_agents/templates/image_upscaler.rb.tt +38 -0
  65. data/lib/generators/ruby_llm_agents/templates/image_variator.rb.tt +33 -0
  66. data/lib/generators/ruby_llm_agents/templates/skills/AGENTS.md.tt +228 -0
  67. data/lib/generators/ruby_llm_agents/templates/skills/BACKGROUND_REMOVERS.md.tt +131 -0
  68. data/lib/generators/ruby_llm_agents/templates/skills/EMBEDDERS.md.tt +255 -0
  69. data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_ANALYZERS.md.tt +120 -0
  70. data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_EDITORS.md.tt +102 -0
  71. data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_GENERATORS.md.tt +282 -0
  72. data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_PIPELINES.md.tt +228 -0
  73. data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_TRANSFORMERS.md.tt +120 -0
  74. data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_UPSCALERS.md.tt +110 -0
  75. data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_VARIATORS.md.tt +120 -0
  76. data/lib/generators/ruby_llm_agents/templates/skills/SPEAKERS.md.tt +212 -0
  77. data/lib/generators/ruby_llm_agents/templates/skills/TOOLS.md.tt +227 -0
  78. data/lib/generators/ruby_llm_agents/templates/skills/TRANSCRIBERS.md.tt +251 -0
  79. data/lib/generators/ruby_llm_agents/templates/skills/WORKFLOWS.md.tt +300 -0
  80. data/lib/generators/ruby_llm_agents/templates/speaker.rb.tt +56 -0
  81. data/lib/generators/ruby_llm_agents/templates/transcriber.rb.tt +51 -0
  82. data/lib/generators/ruby_llm_agents/transcriber_generator.rb +107 -0
  83. data/lib/generators/ruby_llm_agents/upgrade_generator.rb +152 -1
  84. data/lib/ruby_llm/agents/audio/speaker.rb +553 -0
  85. data/lib/ruby_llm/agents/audio/transcriber.rb +669 -0
  86. data/lib/ruby_llm/agents/base_agent.rb +675 -0
  87. data/lib/ruby_llm/agents/core/base/moderation_dsl.rb +181 -0
  88. data/lib/ruby_llm/agents/core/base/moderation_execution.rb +274 -0
  89. data/lib/ruby_llm/agents/core/base.rb +135 -0
  90. data/lib/ruby_llm/agents/core/configuration.rb +981 -0
  91. data/lib/ruby_llm/agents/core/errors.rb +150 -0
  92. data/lib/ruby_llm/agents/{instrumentation.rb → core/instrumentation.rb} +22 -1
  93. data/lib/ruby_llm/agents/core/llm_tenant.rb +358 -0
  94. data/lib/ruby_llm/agents/{version.rb → core/version.rb} +1 -1
  95. data/lib/ruby_llm/agents/dsl/base.rb +110 -0
  96. data/lib/ruby_llm/agents/dsl/caching.rb +142 -0
  97. data/lib/ruby_llm/agents/dsl/reliability.rb +307 -0
  98. data/lib/ruby_llm/agents/dsl.rb +41 -0
  99. data/lib/ruby_llm/agents/image/analyzer/dsl.rb +130 -0
  100. data/lib/ruby_llm/agents/image/analyzer/execution.rb +402 -0
  101. data/lib/ruby_llm/agents/image/analyzer.rb +90 -0
  102. data/lib/ruby_llm/agents/image/background_remover/dsl.rb +154 -0
  103. data/lib/ruby_llm/agents/image/background_remover/execution.rb +240 -0
  104. data/lib/ruby_llm/agents/image/background_remover.rb +89 -0
  105. data/lib/ruby_llm/agents/image/concerns/image_operation_dsl.rb +91 -0
  106. data/lib/ruby_llm/agents/image/concerns/image_operation_execution.rb +165 -0
  107. data/lib/ruby_llm/agents/image/editor/dsl.rb +56 -0
  108. data/lib/ruby_llm/agents/image/editor/execution.rb +207 -0
  109. data/lib/ruby_llm/agents/image/editor.rb +92 -0
  110. data/lib/ruby_llm/agents/image/generator/active_storage_support.rb +127 -0
  111. data/lib/ruby_llm/agents/image/generator/content_policy.rb +95 -0
  112. data/lib/ruby_llm/agents/image/generator/pricing.rb +353 -0
  113. data/lib/ruby_llm/agents/image/generator/templates.rb +124 -0
  114. data/lib/ruby_llm/agents/image/generator.rb +455 -0
  115. data/lib/ruby_llm/agents/image/pipeline/dsl.rb +213 -0
  116. data/lib/ruby_llm/agents/image/pipeline/execution.rb +382 -0
  117. data/lib/ruby_llm/agents/image/pipeline.rb +97 -0
  118. data/lib/ruby_llm/agents/image/transformer/dsl.rb +148 -0
  119. data/lib/ruby_llm/agents/image/transformer/execution.rb +223 -0
  120. data/lib/ruby_llm/agents/image/transformer.rb +95 -0
  121. data/lib/ruby_llm/agents/image/upscaler/dsl.rb +83 -0
  122. data/lib/ruby_llm/agents/image/upscaler/execution.rb +219 -0
  123. data/lib/ruby_llm/agents/image/upscaler.rb +81 -0
  124. data/lib/ruby_llm/agents/image/variator/dsl.rb +62 -0
  125. data/lib/ruby_llm/agents/image/variator/execution.rb +189 -0
  126. data/lib/ruby_llm/agents/image/variator.rb +80 -0
  127. data/lib/ruby_llm/agents/{alert_manager.rb → infrastructure/alert_manager.rb} +17 -22
  128. data/lib/ruby_llm/agents/infrastructure/budget/budget_query.rb +145 -0
  129. data/lib/ruby_llm/agents/infrastructure/budget/config_resolver.rb +149 -0
  130. data/lib/ruby_llm/agents/infrastructure/budget/forecaster.rb +68 -0
  131. data/lib/ruby_llm/agents/infrastructure/budget/spend_recorder.rb +279 -0
  132. data/lib/ruby_llm/agents/infrastructure/budget_tracker.rb +275 -0
  133. data/lib/ruby_llm/agents/{execution_logger_job.rb → infrastructure/execution_logger_job.rb} +17 -1
  134. data/lib/ruby_llm/agents/{reliability → infrastructure/reliability}/executor.rb +2 -1
  135. data/lib/ruby_llm/agents/{reliability → infrastructure/reliability}/retry_strategy.rb +9 -3
  136. data/lib/ruby_llm/agents/{reliability.rb → infrastructure/reliability.rb} +11 -21
  137. data/lib/ruby_llm/agents/pipeline/builder.rb +215 -0
  138. data/lib/ruby_llm/agents/pipeline/context.rb +255 -0
  139. data/lib/ruby_llm/agents/pipeline/executor.rb +86 -0
  140. data/lib/ruby_llm/agents/pipeline/middleware/base.rb +124 -0
  141. data/lib/ruby_llm/agents/pipeline/middleware/budget.rb +95 -0
  142. data/lib/ruby_llm/agents/pipeline/middleware/cache.rb +171 -0
  143. data/lib/ruby_llm/agents/pipeline/middleware/instrumentation.rb +415 -0
  144. data/lib/ruby_llm/agents/pipeline/middleware/reliability.rb +276 -0
  145. data/lib/ruby_llm/agents/pipeline/middleware/tenant.rb +196 -0
  146. data/lib/ruby_llm/agents/pipeline.rb +68 -0
  147. data/lib/ruby_llm/agents/{engine.rb → rails/engine.rb} +79 -11
  148. data/lib/ruby_llm/agents/results/background_removal_result.rb +286 -0
  149. data/lib/ruby_llm/agents/{result.rb → results/base.rb} +73 -1
  150. data/lib/ruby_llm/agents/results/embedding_result.rb +243 -0
  151. data/lib/ruby_llm/agents/results/image_analysis_result.rb +314 -0
  152. data/lib/ruby_llm/agents/results/image_edit_result.rb +250 -0
  153. data/lib/ruby_llm/agents/results/image_generation_result.rb +346 -0
  154. data/lib/ruby_llm/agents/results/image_pipeline_result.rb +399 -0
  155. data/lib/ruby_llm/agents/results/image_transform_result.rb +251 -0
  156. data/lib/ruby_llm/agents/results/image_upscale_result.rb +255 -0
  157. data/lib/ruby_llm/agents/results/image_variation_result.rb +237 -0
  158. data/lib/ruby_llm/agents/results/moderation_result.rb +158 -0
  159. data/lib/ruby_llm/agents/results/speech_result.rb +338 -0
  160. data/lib/ruby_llm/agents/results/transcription_result.rb +408 -0
  161. data/lib/ruby_llm/agents/text/embedder.rb +444 -0
  162. data/lib/ruby_llm/agents/text/moderator.rb +237 -0
  163. data/lib/ruby_llm/agents/workflow/async.rb +220 -0
  164. data/lib/ruby_llm/agents/workflow/async_executor.rb +156 -0
  165. data/lib/ruby_llm/agents/{workflow.rb → workflow/orchestrator.rb} +6 -5
  166. data/lib/ruby_llm/agents/workflow/parallel.rb +34 -17
  167. data/lib/ruby_llm/agents/workflow/thread_pool.rb +185 -0
  168. data/lib/ruby_llm/agents.rb +86 -20
  169. metadata +172 -34
  170. data/lib/ruby_llm/agents/base/caching.rb +0 -40
  171. data/lib/ruby_llm/agents/base/cost_calculation.rb +0 -105
  172. data/lib/ruby_llm/agents/base/dsl.rb +0 -324
  173. data/lib/ruby_llm/agents/base/execution.rb +0 -366
  174. data/lib/ruby_llm/agents/base/reliability_dsl.rb +0 -82
  175. data/lib/ruby_llm/agents/base/reliability_execution.rb +0 -136
  176. data/lib/ruby_llm/agents/base/response_building.rb +0 -86
  177. data/lib/ruby_llm/agents/base/tool_tracking.rb +0 -57
  178. data/lib/ruby_llm/agents/base.rb +0 -210
  179. data/lib/ruby_llm/agents/budget_tracker.rb +0 -733
  180. data/lib/ruby_llm/agents/configuration.rb +0 -394
  181. /data/lib/ruby_llm/agents/{deprecations.rb → core/deprecations.rb} +0 -0
  182. /data/lib/ruby_llm/agents/{inflections.rb → core/inflections.rb} +0 -0
  183. /data/lib/ruby_llm/agents/{resolved_config.rb → core/resolved_config.rb} +0 -0
  184. /data/lib/ruby_llm/agents/{attempt_tracker.rb → infrastructure/attempt_tracker.rb} +0 -0
  185. /data/lib/ruby_llm/agents/{cache_helper.rb → infrastructure/cache_helper.rb} +0 -0
  186. /data/lib/ruby_llm/agents/{circuit_breaker.rb → infrastructure/circuit_breaker.rb} +0 -0
  187. /data/lib/ruby_llm/agents/{redactor.rb → infrastructure/redactor.rb} +0 -0
  188. /data/lib/ruby_llm/agents/{reliability → infrastructure/reliability}/breaker_manager.rb +0 -0
  189. /data/lib/ruby_llm/agents/{reliability → infrastructure/reliability}/execution_constraints.rb +0 -0
  190. /data/lib/ruby_llm/agents/{reliability → infrastructure/reliability}/fallback_routing.rb +0 -0
@@ -0,0 +1,255 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module Agents
5
+ # Result wrapper for image upscaling operations
6
+ #
7
+ # Provides a consistent interface for accessing upscaled images,
8
+ # metadata, timing, and cost information.
9
+ #
10
+ # @example Accessing upscaled image
11
+ # result = ImageUpscaler.call(image: "low_res.jpg", scale: 4)
12
+ # result.url # => "https://..."
13
+ # result.scale # => 4
14
+ # result.output_size # => "4096x4096"
15
+ # result.success? # => true
16
+ #
17
+ class ImageUpscaleResult
18
+ attr_reader :image, :source_image, :model_id, :scale, :output_size, :face_enhance,
19
+ :started_at, :completed_at, :tenant_id, :upscaler_class,
20
+ :error_class, :error_message
21
+
22
+ # Initialize a new result
23
+ #
24
+ # @param image [Object] The upscaled image object
25
+ # @param source_image [String] The original source image
26
+ # @param model_id [String] Model used for upscaling
27
+ # @param scale [Integer] Upscale factor used
28
+ # @param output_size [String] Output image dimensions
29
+ # @param face_enhance [Boolean] Whether face enhancement was used
30
+ # @param started_at [Time] When upscaling started
31
+ # @param completed_at [Time] When upscaling completed
32
+ # @param tenant_id [String, nil] Tenant identifier
33
+ # @param upscaler_class [String] Name of the upscaler class
34
+ # @param error_class [String, nil] Error class name if failed
35
+ # @param error_message [String, nil] Error message if failed
36
+ def initialize(image:, source_image:, model_id:, scale:, output_size:, face_enhance:,
37
+ started_at:, completed_at:, tenant_id:, upscaler_class:,
38
+ error_class: nil, error_message: nil)
39
+ @image = image
40
+ @source_image = source_image
41
+ @model_id = model_id
42
+ @scale = scale
43
+ @output_size = output_size
44
+ @face_enhance = face_enhance
45
+ @started_at = started_at
46
+ @completed_at = completed_at
47
+ @tenant_id = tenant_id
48
+ @upscaler_class = upscaler_class
49
+ @error_class = error_class
50
+ @error_message = error_message
51
+ end
52
+
53
+ # Status helpers
54
+
55
+ def success?
56
+ error_class.nil? && !image.nil?
57
+ end
58
+
59
+ def error?
60
+ !success?
61
+ end
62
+
63
+ # Always single image for upscaling
64
+ def single?
65
+ true
66
+ end
67
+
68
+ def batch?
69
+ false
70
+ end
71
+
72
+ # Image access
73
+
74
+ def url
75
+ image&.url
76
+ end
77
+
78
+ def urls
79
+ success? ? [url].compact : []
80
+ end
81
+
82
+ def data
83
+ image&.data
84
+ end
85
+
86
+ def datas
87
+ success? ? [data].compact : []
88
+ end
89
+
90
+ def base64?
91
+ image&.base64? || false
92
+ end
93
+
94
+ def mime_type
95
+ image&.mime_type
96
+ end
97
+
98
+ # Count (always 1 for upscaling)
99
+
100
+ def count
101
+ success? ? 1 : 0
102
+ end
103
+
104
+ # Size helpers
105
+
106
+ def size
107
+ output_size
108
+ end
109
+
110
+ def output_width
111
+ return nil unless output_size
112
+ output_size.split("x").first.to_i
113
+ end
114
+
115
+ def output_height
116
+ return nil unless output_size
117
+ output_size.split("x").last.to_i
118
+ end
119
+
120
+ # Timing
121
+
122
+ def duration_ms
123
+ return 0 unless started_at && completed_at
124
+ ((completed_at - started_at) * 1000).round
125
+ end
126
+
127
+ # Cost estimation
128
+
129
+ def total_cost
130
+ return 0 if error?
131
+
132
+ # Upscaling typically has fixed per-image cost
133
+ ImageGenerator::Pricing.calculate_cost(
134
+ model_id: model_id,
135
+ count: 1
136
+ )
137
+ end
138
+
139
+ # File operations
140
+
141
+ def save(path)
142
+ raise "No image to save" unless image
143
+ image.save(path)
144
+ end
145
+
146
+ def to_blob
147
+ image&.to_blob
148
+ end
149
+
150
+ def blobs
151
+ success? ? [to_blob].compact : []
152
+ end
153
+
154
+ # Serialization
155
+
156
+ def to_h
157
+ {
158
+ success: success?,
159
+ url: url,
160
+ base64: base64?,
161
+ mime_type: mime_type,
162
+ source_image: source_image,
163
+ model_id: model_id,
164
+ scale: scale,
165
+ output_size: output_size,
166
+ face_enhance: face_enhance,
167
+ total_cost: total_cost,
168
+ duration_ms: duration_ms,
169
+ started_at: started_at&.iso8601,
170
+ completed_at: completed_at&.iso8601,
171
+ tenant_id: tenant_id,
172
+ upscaler_class: upscaler_class,
173
+ error_class: error_class,
174
+ error_message: error_message
175
+ }
176
+ end
177
+
178
+ # Caching
179
+
180
+ def to_cache
181
+ {
182
+ url: url,
183
+ data: data,
184
+ mime_type: mime_type,
185
+ model_id: model_id,
186
+ scale: scale,
187
+ output_size: output_size,
188
+ total_cost: total_cost,
189
+ cached_at: Time.current.iso8601
190
+ }
191
+ end
192
+
193
+ def self.from_cache(data)
194
+ CachedImageUpscaleResult.new(data)
195
+ end
196
+ end
197
+
198
+ # Lightweight result for cached upscales
199
+ class CachedImageUpscaleResult
200
+ attr_reader :url, :data, :mime_type, :model_id, :scale, :output_size,
201
+ :total_cost, :cached_at
202
+
203
+ def initialize(data)
204
+ @url = data[:url]
205
+ @data = data[:data]
206
+ @mime_type = data[:mime_type]
207
+ @model_id = data[:model_id]
208
+ @scale = data[:scale]
209
+ @output_size = data[:output_size]
210
+ @total_cost = data[:total_cost]
211
+ @cached_at = data[:cached_at]
212
+ end
213
+
214
+ def success?
215
+ !url.nil? || !data.nil?
216
+ end
217
+
218
+ def error?
219
+ !success?
220
+ end
221
+
222
+ def cached?
223
+ true
224
+ end
225
+
226
+ def urls
227
+ success? ? [url].compact : []
228
+ end
229
+
230
+ def datas
231
+ success? ? [data].compact : []
232
+ end
233
+
234
+ def base64?
235
+ !data.nil?
236
+ end
237
+
238
+ def count
239
+ success? ? 1 : 0
240
+ end
241
+
242
+ def single?
243
+ true
244
+ end
245
+
246
+ def batch?
247
+ false
248
+ end
249
+
250
+ def size
251
+ output_size
252
+ end
253
+ end
254
+ end
255
+ end
@@ -0,0 +1,237 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module Agents
5
+ # Result wrapper for image variation operations
6
+ #
7
+ # Provides a consistent interface for accessing variation images,
8
+ # metadata, timing, and cost information.
9
+ #
10
+ # @example Accessing variations
11
+ # result = ImageVariator.call(image: "logo.png", count: 4)
12
+ # result.urls # => ["https://...", ...]
13
+ # result.count # => 4
14
+ # result.success? # => true
15
+ #
16
+ class ImageVariationResult
17
+ attr_reader :images, :source_image, :model_id, :size, :variation_strength,
18
+ :started_at, :completed_at, :tenant_id, :variator_class,
19
+ :error_class, :error_message
20
+
21
+ # Initialize a new result
22
+ #
23
+ # @param images [Array<Object>] Array of variation image objects
24
+ # @param source_image [String] The original source image
25
+ # @param model_id [String] Model used for variation
26
+ # @param size [String] Image size
27
+ # @param variation_strength [Float] Variation strength used
28
+ # @param started_at [Time] When variation started
29
+ # @param completed_at [Time] When variation completed
30
+ # @param tenant_id [String, nil] Tenant identifier
31
+ # @param variator_class [String] Name of the variator class
32
+ # @param error_class [String, nil] Error class name if failed
33
+ # @param error_message [String, nil] Error message if failed
34
+ def initialize(images:, source_image:, model_id:, size:, variation_strength:,
35
+ started_at:, completed_at:, tenant_id:, variator_class:,
36
+ error_class: nil, error_message: nil)
37
+ @images = images
38
+ @source_image = source_image
39
+ @model_id = model_id
40
+ @size = size
41
+ @variation_strength = variation_strength
42
+ @started_at = started_at
43
+ @completed_at = completed_at
44
+ @tenant_id = tenant_id
45
+ @variator_class = variator_class
46
+ @error_class = error_class
47
+ @error_message = error_message
48
+ end
49
+
50
+ # Status helpers
51
+
52
+ def success?
53
+ error_class.nil? && images.any?
54
+ end
55
+
56
+ def error?
57
+ !success?
58
+ end
59
+
60
+ def single?
61
+ count == 1
62
+ end
63
+
64
+ def batch?
65
+ count > 1
66
+ end
67
+
68
+ # Image access
69
+
70
+ def image
71
+ images.first
72
+ end
73
+
74
+ def url
75
+ image&.url
76
+ end
77
+
78
+ def urls
79
+ images.map(&:url).compact
80
+ end
81
+
82
+ def data
83
+ image&.data
84
+ end
85
+
86
+ def datas
87
+ images.map(&:data).compact
88
+ end
89
+
90
+ def base64?
91
+ image&.base64? || false
92
+ end
93
+
94
+ def mime_type
95
+ image&.mime_type
96
+ end
97
+
98
+ # Count
99
+
100
+ def count
101
+ images.size
102
+ end
103
+
104
+ # Timing
105
+
106
+ def duration_ms
107
+ return 0 unless started_at && completed_at
108
+ ((completed_at - started_at) * 1000).round
109
+ end
110
+
111
+ # Cost estimation
112
+
113
+ def total_cost
114
+ return 0 if error?
115
+
116
+ ImageGenerator::Pricing.calculate_cost(
117
+ model_id: model_id,
118
+ size: size,
119
+ count: count
120
+ )
121
+ end
122
+
123
+ # File operations
124
+
125
+ def save(path)
126
+ raise "No image to save" unless image
127
+ image.save(path)
128
+ end
129
+
130
+ def save_all(directory, prefix: "variation")
131
+ images.each_with_index do |img, idx|
132
+ filename = "#{prefix}_#{idx + 1}.png"
133
+ img.save(File.join(directory, filename))
134
+ end
135
+ end
136
+
137
+ def to_blob
138
+ image&.to_blob
139
+ end
140
+
141
+ def blobs
142
+ images.map(&:to_blob)
143
+ end
144
+
145
+ # Serialization
146
+
147
+ def to_h
148
+ {
149
+ success: success?,
150
+ count: count,
151
+ urls: urls,
152
+ base64: base64?,
153
+ mime_type: mime_type,
154
+ source_image: source_image,
155
+ model_id: model_id,
156
+ size: size,
157
+ variation_strength: variation_strength,
158
+ total_cost: total_cost,
159
+ duration_ms: duration_ms,
160
+ started_at: started_at&.iso8601,
161
+ completed_at: completed_at&.iso8601,
162
+ tenant_id: tenant_id,
163
+ variator_class: variator_class,
164
+ error_class: error_class,
165
+ error_message: error_message
166
+ }
167
+ end
168
+
169
+ # Caching
170
+
171
+ def to_cache
172
+ {
173
+ urls: urls,
174
+ datas: datas,
175
+ mime_type: mime_type,
176
+ model_id: model_id,
177
+ total_cost: total_cost,
178
+ cached_at: Time.current.iso8601
179
+ }
180
+ end
181
+
182
+ def self.from_cache(data)
183
+ CachedImageVariationResult.new(data)
184
+ end
185
+ end
186
+
187
+ # Lightweight result for cached variations
188
+ class CachedImageVariationResult
189
+ attr_reader :urls, :datas, :mime_type, :model_id, :total_cost, :cached_at
190
+
191
+ def initialize(data)
192
+ @urls = data[:urls] || []
193
+ @datas = data[:datas] || []
194
+ @mime_type = data[:mime_type]
195
+ @model_id = data[:model_id]
196
+ @total_cost = data[:total_cost]
197
+ @cached_at = data[:cached_at]
198
+ end
199
+
200
+ def success?
201
+ urls.any? || datas.any?
202
+ end
203
+
204
+ def error?
205
+ !success?
206
+ end
207
+
208
+ def cached?
209
+ true
210
+ end
211
+
212
+ def url
213
+ urls.first
214
+ end
215
+
216
+ def data
217
+ datas.first
218
+ end
219
+
220
+ def base64?
221
+ datas.any?
222
+ end
223
+
224
+ def count
225
+ [urls.size, datas.size].max
226
+ end
227
+
228
+ def single?
229
+ count == 1
230
+ end
231
+
232
+ def batch?
233
+ count > 1
234
+ end
235
+ end
236
+ end
237
+ end
@@ -0,0 +1,158 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module Agents
5
+ # Wrapper for moderation results with threshold and category filtering
6
+ #
7
+ # Provides a filtered view of moderation results based on configured
8
+ # thresholds and category filters. The raw result is still accessible
9
+ # for full details.
10
+ #
11
+ # @example Basic usage
12
+ # result = ModerationResult.new(
13
+ # result: raw_moderation,
14
+ # threshold: 0.8,
15
+ # categories: [:hate, :violence]
16
+ # )
17
+ #
18
+ # result.flagged? # Only true if score >= 0.8 AND category is hate or violence
19
+ # result.passed? # Opposite of flagged?
20
+ #
21
+ # @api public
22
+ class ModerationResult
23
+ # @return [Object] The raw moderation result from RubyLLM
24
+ attr_reader :raw_result
25
+
26
+ # @return [Float, nil] Configured threshold for flagging
27
+ attr_reader :threshold
28
+
29
+ # @return [Array<Symbol>, nil] Categories to filter on
30
+ attr_reader :filter_categories
31
+
32
+ # Creates a new ModerationResult
33
+ #
34
+ # @param result [Object] Raw moderation result from RubyLLM
35
+ # @param threshold [Float, nil] Score threshold (0.0-1.0)
36
+ # @param categories [Array<Symbol>, nil] Categories to check
37
+ def initialize(result:, threshold: nil, categories: nil)
38
+ @raw_result = result
39
+ @threshold = threshold
40
+ @filter_categories = categories&.map(&:to_sym)
41
+ end
42
+
43
+ # Returns whether the content should be flagged
44
+ #
45
+ # Considers both threshold and category filters if configured.
46
+ # Content is flagged only if:
47
+ # - Raw result is flagged AND
48
+ # - Score meets threshold (if configured) AND
49
+ # - Category matches filter (if configured)
50
+ #
51
+ # @return [Boolean] true if content should be flagged
52
+ def flagged?
53
+ return false unless raw_result.flagged?
54
+
55
+ passes_threshold? && passes_category_filter?
56
+ end
57
+
58
+ # Returns whether the content passed moderation
59
+ #
60
+ # @return [Boolean] true if content is not flagged
61
+ def passed?
62
+ !flagged?
63
+ end
64
+
65
+ # Returns the flagged categories, filtered by configuration
66
+ #
67
+ # @return [Array<String, Symbol>] Categories that triggered flagging
68
+ def flagged_categories
69
+ cats = raw_result.flagged_categories || []
70
+ return cats unless filter_categories&.any?
71
+
72
+ cats.select { |c| filter_categories.include?(normalize_category(c)) }
73
+ end
74
+
75
+ # Returns all category scores from the raw result
76
+ #
77
+ # @return [Hash{String, Symbol => Float}] Category to score mapping
78
+ def category_scores
79
+ raw_result.category_scores || {}
80
+ end
81
+
82
+ # Returns the moderation result ID
83
+ #
84
+ # @return [String, nil] Result identifier
85
+ def id
86
+ raw_result.id
87
+ end
88
+
89
+ # Returns the model used for moderation
90
+ #
91
+ # @return [String, nil] Model identifier
92
+ def model
93
+ raw_result.model
94
+ end
95
+
96
+ # Returns the maximum score across all categories
97
+ #
98
+ # @return [Float] Highest category score
99
+ def max_score
100
+ scores = category_scores.values
101
+ scores.any? ? scores.max : 0.0
102
+ end
103
+
104
+ # Returns whether the raw result was flagged (ignoring filters)
105
+ #
106
+ # @return [Boolean] true if raw result was flagged
107
+ def raw_flagged?
108
+ raw_result.flagged?
109
+ end
110
+
111
+ # Converts the result to a hash
112
+ #
113
+ # @return [Hash] Result data as a hash
114
+ def to_h
115
+ {
116
+ flagged: flagged?,
117
+ raw_flagged: raw_flagged?,
118
+ flagged_categories: flagged_categories,
119
+ category_scores: category_scores,
120
+ max_score: max_score,
121
+ threshold: threshold,
122
+ filter_categories: filter_categories,
123
+ model: model,
124
+ id: id
125
+ }
126
+ end
127
+
128
+ private
129
+
130
+ # Checks if the max score meets the threshold
131
+ #
132
+ # @return [Boolean] true if threshold is met or not configured
133
+ def passes_threshold?
134
+ return true unless threshold
135
+
136
+ max_score >= threshold
137
+ end
138
+
139
+ # Checks if any flagged categories match the filter
140
+ #
141
+ # @return [Boolean] true if categories match or no filter configured
142
+ def passes_category_filter?
143
+ return true unless filter_categories&.any?
144
+
145
+ normalized_flagged = (raw_result.flagged_categories || []).map { |c| normalize_category(c) }
146
+ (normalized_flagged & filter_categories).any?
147
+ end
148
+
149
+ # Normalizes category names for comparison
150
+ #
151
+ # @param category [String, Symbol] Category name
152
+ # @return [Symbol] Normalized category symbol
153
+ def normalize_category(category)
154
+ category.to_s.tr("/", "_").tr("-", "_").downcase.to_sym
155
+ end
156
+ end
157
+ end
158
+ end