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