ruby_llm-agents 0.5.0 → 1.0.0
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.
- checksums.yaml +4 -4
- data/README.md +189 -31
- data/app/controllers/ruby_llm/agents/agents_controller.rb +136 -16
- data/app/controllers/ruby_llm/agents/dashboard_controller.rb +29 -9
- data/app/controllers/ruby_llm/agents/workflows_controller.rb +355 -0
- data/app/helpers/ruby_llm/agents/application_helper.rb +25 -0
- data/app/models/ruby_llm/agents/execution.rb +3 -0
- data/app/models/ruby_llm/agents/tenant_budget.rb +58 -15
- data/app/services/ruby_llm/agents/agent_registry.rb +51 -12
- data/app/views/layouts/ruby_llm/agents/application.html.erb +2 -29
- data/app/views/ruby_llm/agents/agents/_agent.html.erb +13 -1
- data/app/views/ruby_llm/agents/agents/_config_agent.html.erb +235 -0
- data/app/views/ruby_llm/agents/agents/_config_embedder.html.erb +70 -0
- data/app/views/ruby_llm/agents/agents/_config_image_generator.html.erb +152 -0
- data/app/views/ruby_llm/agents/agents/_config_moderator.html.erb +63 -0
- data/app/views/ruby_llm/agents/agents/_config_speaker.html.erb +108 -0
- data/app/views/ruby_llm/agents/agents/_config_transcriber.html.erb +91 -0
- data/app/views/ruby_llm/agents/agents/_workflow.html.erb +1 -1
- data/app/views/ruby_llm/agents/agents/index.html.erb +74 -9
- data/app/views/ruby_llm/agents/agents/show.html.erb +18 -378
- data/app/views/ruby_llm/agents/dashboard/_agent_comparison.html.erb +269 -15
- data/app/views/ruby_llm/agents/executions/show.html.erb +16 -0
- data/app/views/ruby_llm/agents/shared/_agent_type_badge.html.erb +93 -0
- data/app/views/ruby_llm/agents/workflows/_step_performance.html.erb +236 -0
- data/app/views/ruby_llm/agents/workflows/_structure_parallel.html.erb +76 -0
- data/app/views/ruby_llm/agents/workflows/_structure_pipeline.html.erb +74 -0
- data/app/views/ruby_llm/agents/workflows/_structure_router.html.erb +108 -0
- data/app/views/ruby_llm/agents/workflows/show.html.erb +442 -0
- data/config/routes.rb +1 -0
- data/lib/generators/ruby_llm_agents/agent_generator.rb +56 -7
- data/lib/generators/ruby_llm_agents/background_remover_generator.rb +110 -0
- data/lib/generators/ruby_llm_agents/embedder_generator.rb +107 -0
- data/lib/generators/ruby_llm_agents/image_analyzer_generator.rb +115 -0
- data/lib/generators/ruby_llm_agents/image_editor_generator.rb +108 -0
- data/lib/generators/ruby_llm_agents/image_generator_generator.rb +116 -0
- data/lib/generators/ruby_llm_agents/image_pipeline_generator.rb +178 -0
- data/lib/generators/ruby_llm_agents/image_transformer_generator.rb +109 -0
- data/lib/generators/ruby_llm_agents/image_upscaler_generator.rb +103 -0
- data/lib/generators/ruby_llm_agents/image_variator_generator.rb +102 -0
- data/lib/generators/ruby_llm_agents/install_generator.rb +76 -4
- data/lib/generators/ruby_llm_agents/restructure_generator.rb +292 -0
- data/lib/generators/ruby_llm_agents/speaker_generator.rb +121 -0
- data/lib/generators/ruby_llm_agents/templates/add_execution_type_migration.rb.tt +8 -0
- data/lib/generators/ruby_llm_agents/templates/agent.rb.tt +99 -84
- data/lib/generators/ruby_llm_agents/templates/application_agent.rb.tt +42 -40
- data/lib/generators/ruby_llm_agents/templates/application_background_remover.rb.tt +26 -0
- data/lib/generators/ruby_llm_agents/templates/application_embedder.rb.tt +50 -0
- data/lib/generators/ruby_llm_agents/templates/application_image_analyzer.rb.tt +26 -0
- data/lib/generators/ruby_llm_agents/templates/application_image_editor.rb.tt +20 -0
- data/lib/generators/ruby_llm_agents/templates/application_image_generator.rb.tt +38 -0
- data/lib/generators/ruby_llm_agents/templates/application_image_pipeline.rb.tt +139 -0
- data/lib/generators/ruby_llm_agents/templates/application_image_transformer.rb.tt +21 -0
- data/lib/generators/ruby_llm_agents/templates/application_image_upscaler.rb.tt +20 -0
- data/lib/generators/ruby_llm_agents/templates/application_image_variator.rb.tt +20 -0
- data/lib/generators/ruby_llm_agents/templates/application_speaker.rb.tt +49 -0
- data/lib/generators/ruby_llm_agents/templates/application_transcriber.rb.tt +53 -0
- data/lib/generators/ruby_llm_agents/templates/background_remover.rb.tt +44 -0
- data/lib/generators/ruby_llm_agents/templates/embedder.rb.tt +41 -0
- data/lib/generators/ruby_llm_agents/templates/image_analyzer.rb.tt +45 -0
- data/lib/generators/ruby_llm_agents/templates/image_editor.rb.tt +35 -0
- data/lib/generators/ruby_llm_agents/templates/image_generator.rb.tt +47 -0
- data/lib/generators/ruby_llm_agents/templates/image_pipeline.rb.tt +50 -0
- data/lib/generators/ruby_llm_agents/templates/image_transformer.rb.tt +44 -0
- data/lib/generators/ruby_llm_agents/templates/image_upscaler.rb.tt +38 -0
- data/lib/generators/ruby_llm_agents/templates/image_variator.rb.tt +33 -0
- data/lib/generators/ruby_llm_agents/templates/skills/AGENTS.md.tt +228 -0
- data/lib/generators/ruby_llm_agents/templates/skills/BACKGROUND_REMOVERS.md.tt +131 -0
- data/lib/generators/ruby_llm_agents/templates/skills/EMBEDDERS.md.tt +255 -0
- data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_ANALYZERS.md.tt +120 -0
- data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_EDITORS.md.tt +102 -0
- data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_GENERATORS.md.tt +282 -0
- data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_PIPELINES.md.tt +228 -0
- data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_TRANSFORMERS.md.tt +120 -0
- data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_UPSCALERS.md.tt +110 -0
- data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_VARIATORS.md.tt +120 -0
- data/lib/generators/ruby_llm_agents/templates/skills/SPEAKERS.md.tt +212 -0
- data/lib/generators/ruby_llm_agents/templates/skills/TOOLS.md.tt +227 -0
- data/lib/generators/ruby_llm_agents/templates/skills/TRANSCRIBERS.md.tt +251 -0
- data/lib/generators/ruby_llm_agents/templates/skills/WORKFLOWS.md.tt +300 -0
- data/lib/generators/ruby_llm_agents/templates/speaker.rb.tt +56 -0
- data/lib/generators/ruby_llm_agents/templates/transcriber.rb.tt +51 -0
- data/lib/generators/ruby_llm_agents/transcriber_generator.rb +107 -0
- data/lib/generators/ruby_llm_agents/upgrade_generator.rb +152 -1
- data/lib/ruby_llm/agents/audio/speaker.rb +553 -0
- data/lib/ruby_llm/agents/audio/transcriber.rb +669 -0
- data/lib/ruby_llm/agents/base_agent.rb +675 -0
- data/lib/ruby_llm/agents/core/base/moderation_dsl.rb +181 -0
- data/lib/ruby_llm/agents/core/base/moderation_execution.rb +274 -0
- data/lib/ruby_llm/agents/core/base.rb +135 -0
- data/lib/ruby_llm/agents/core/configuration.rb +981 -0
- data/lib/ruby_llm/agents/core/errors.rb +150 -0
- data/lib/ruby_llm/agents/{instrumentation.rb → core/instrumentation.rb} +22 -1
- data/lib/ruby_llm/agents/core/llm_tenant.rb +358 -0
- data/lib/ruby_llm/agents/{version.rb → core/version.rb} +1 -1
- data/lib/ruby_llm/agents/dsl/base.rb +110 -0
- data/lib/ruby_llm/agents/dsl/caching.rb +142 -0
- data/lib/ruby_llm/agents/dsl/reliability.rb +307 -0
- data/lib/ruby_llm/agents/dsl.rb +41 -0
- data/lib/ruby_llm/agents/image/analyzer/dsl.rb +130 -0
- data/lib/ruby_llm/agents/image/analyzer/execution.rb +402 -0
- data/lib/ruby_llm/agents/image/analyzer.rb +90 -0
- data/lib/ruby_llm/agents/image/background_remover/dsl.rb +154 -0
- data/lib/ruby_llm/agents/image/background_remover/execution.rb +240 -0
- data/lib/ruby_llm/agents/image/background_remover.rb +89 -0
- data/lib/ruby_llm/agents/image/concerns/image_operation_dsl.rb +91 -0
- data/lib/ruby_llm/agents/image/concerns/image_operation_execution.rb +165 -0
- data/lib/ruby_llm/agents/image/editor/dsl.rb +56 -0
- data/lib/ruby_llm/agents/image/editor/execution.rb +207 -0
- data/lib/ruby_llm/agents/image/editor.rb +92 -0
- data/lib/ruby_llm/agents/image/generator/active_storage_support.rb +127 -0
- data/lib/ruby_llm/agents/image/generator/content_policy.rb +95 -0
- data/lib/ruby_llm/agents/image/generator/pricing.rb +353 -0
- data/lib/ruby_llm/agents/image/generator/templates.rb +124 -0
- data/lib/ruby_llm/agents/image/generator.rb +455 -0
- data/lib/ruby_llm/agents/image/pipeline/dsl.rb +213 -0
- data/lib/ruby_llm/agents/image/pipeline/execution.rb +382 -0
- data/lib/ruby_llm/agents/image/pipeline.rb +97 -0
- data/lib/ruby_llm/agents/image/transformer/dsl.rb +148 -0
- data/lib/ruby_llm/agents/image/transformer/execution.rb +223 -0
- data/lib/ruby_llm/agents/image/transformer.rb +95 -0
- data/lib/ruby_llm/agents/image/upscaler/dsl.rb +83 -0
- data/lib/ruby_llm/agents/image/upscaler/execution.rb +219 -0
- data/lib/ruby_llm/agents/image/upscaler.rb +81 -0
- data/lib/ruby_llm/agents/image/variator/dsl.rb +62 -0
- data/lib/ruby_llm/agents/image/variator/execution.rb +189 -0
- data/lib/ruby_llm/agents/image/variator.rb +80 -0
- data/lib/ruby_llm/agents/{alert_manager.rb → infrastructure/alert_manager.rb} +17 -22
- data/lib/ruby_llm/agents/infrastructure/budget/budget_query.rb +145 -0
- data/lib/ruby_llm/agents/infrastructure/budget/config_resolver.rb +149 -0
- data/lib/ruby_llm/agents/infrastructure/budget/forecaster.rb +68 -0
- data/lib/ruby_llm/agents/infrastructure/budget/spend_recorder.rb +279 -0
- data/lib/ruby_llm/agents/infrastructure/budget_tracker.rb +275 -0
- data/lib/ruby_llm/agents/{execution_logger_job.rb → infrastructure/execution_logger_job.rb} +17 -1
- data/lib/ruby_llm/agents/{reliability → infrastructure/reliability}/executor.rb +2 -1
- data/lib/ruby_llm/agents/{reliability → infrastructure/reliability}/retry_strategy.rb +9 -3
- data/lib/ruby_llm/agents/{reliability.rb → infrastructure/reliability.rb} +11 -21
- data/lib/ruby_llm/agents/pipeline/builder.rb +215 -0
- data/lib/ruby_llm/agents/pipeline/context.rb +255 -0
- data/lib/ruby_llm/agents/pipeline/executor.rb +86 -0
- data/lib/ruby_llm/agents/pipeline/middleware/base.rb +124 -0
- data/lib/ruby_llm/agents/pipeline/middleware/budget.rb +95 -0
- data/lib/ruby_llm/agents/pipeline/middleware/cache.rb +171 -0
- data/lib/ruby_llm/agents/pipeline/middleware/instrumentation.rb +415 -0
- data/lib/ruby_llm/agents/pipeline/middleware/reliability.rb +276 -0
- data/lib/ruby_llm/agents/pipeline/middleware/tenant.rb +196 -0
- data/lib/ruby_llm/agents/pipeline.rb +68 -0
- data/lib/ruby_llm/agents/{engine.rb → rails/engine.rb} +79 -11
- data/lib/ruby_llm/agents/results/background_removal_result.rb +286 -0
- data/lib/ruby_llm/agents/{result.rb → results/base.rb} +73 -1
- data/lib/ruby_llm/agents/results/embedding_result.rb +243 -0
- data/lib/ruby_llm/agents/results/image_analysis_result.rb +314 -0
- data/lib/ruby_llm/agents/results/image_edit_result.rb +250 -0
- data/lib/ruby_llm/agents/results/image_generation_result.rb +346 -0
- data/lib/ruby_llm/agents/results/image_pipeline_result.rb +399 -0
- data/lib/ruby_llm/agents/results/image_transform_result.rb +251 -0
- data/lib/ruby_llm/agents/results/image_upscale_result.rb +255 -0
- data/lib/ruby_llm/agents/results/image_variation_result.rb +237 -0
- data/lib/ruby_llm/agents/results/moderation_result.rb +158 -0
- data/lib/ruby_llm/agents/results/speech_result.rb +338 -0
- data/lib/ruby_llm/agents/results/transcription_result.rb +408 -0
- data/lib/ruby_llm/agents/text/embedder.rb +444 -0
- data/lib/ruby_llm/agents/text/moderator.rb +237 -0
- data/lib/ruby_llm/agents/workflow/async.rb +220 -0
- data/lib/ruby_llm/agents/workflow/async_executor.rb +156 -0
- data/lib/ruby_llm/agents/{workflow.rb → workflow/orchestrator.rb} +6 -5
- data/lib/ruby_llm/agents/workflow/parallel.rb +34 -17
- data/lib/ruby_llm/agents/workflow/thread_pool.rb +185 -0
- data/lib/ruby_llm/agents.rb +86 -20
- metadata +172 -34
- data/lib/ruby_llm/agents/base/caching.rb +0 -40
- data/lib/ruby_llm/agents/base/cost_calculation.rb +0 -105
- data/lib/ruby_llm/agents/base/dsl.rb +0 -324
- data/lib/ruby_llm/agents/base/execution.rb +0 -366
- data/lib/ruby_llm/agents/base/reliability_dsl.rb +0 -82
- data/lib/ruby_llm/agents/base/reliability_execution.rb +0 -136
- data/lib/ruby_llm/agents/base/response_building.rb +0 -86
- data/lib/ruby_llm/agents/base/tool_tracking.rb +0 -57
- data/lib/ruby_llm/agents/base.rb +0 -210
- data/lib/ruby_llm/agents/budget_tracker.rb +0 -733
- data/lib/ruby_llm/agents/configuration.rb +0 -394
- /data/lib/ruby_llm/agents/{deprecations.rb → core/deprecations.rb} +0 -0
- /data/lib/ruby_llm/agents/{inflections.rb → core/inflections.rb} +0 -0
- /data/lib/ruby_llm/agents/{resolved_config.rb → core/resolved_config.rb} +0 -0
- /data/lib/ruby_llm/agents/{attempt_tracker.rb → infrastructure/attempt_tracker.rb} +0 -0
- /data/lib/ruby_llm/agents/{cache_helper.rb → infrastructure/cache_helper.rb} +0 -0
- /data/lib/ruby_llm/agents/{circuit_breaker.rb → infrastructure/circuit_breaker.rb} +0 -0
- /data/lib/ruby_llm/agents/{redactor.rb → infrastructure/redactor.rb} +0 -0
- /data/lib/ruby_llm/agents/{reliability → infrastructure/reliability}/breaker_manager.rb +0 -0
- /data/lib/ruby_llm/agents/{reliability → infrastructure/reliability}/execution_constraints.rb +0 -0
- /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
|