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,207 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "digest"
|
|
4
|
+
require_relative "../concerns/image_operation_execution"
|
|
5
|
+
|
|
6
|
+
module RubyLLM
|
|
7
|
+
module Agents
|
|
8
|
+
class ImageEditor
|
|
9
|
+
# Execution logic for image editors
|
|
10
|
+
#
|
|
11
|
+
# Handles image/mask validation, content policy checks,
|
|
12
|
+
# budget tracking, caching, image editing, and result building.
|
|
13
|
+
#
|
|
14
|
+
module Execution
|
|
15
|
+
include Concerns::ImageOperationExecution
|
|
16
|
+
|
|
17
|
+
# Execute the image editing pipeline
|
|
18
|
+
#
|
|
19
|
+
# @return [ImageEditResult] The result containing edited image
|
|
20
|
+
def execute
|
|
21
|
+
started_at = Time.current
|
|
22
|
+
|
|
23
|
+
resolve_tenant_context!
|
|
24
|
+
check_budget! if budget_tracking_enabled?
|
|
25
|
+
validate_inputs!
|
|
26
|
+
validate_content_policy!
|
|
27
|
+
|
|
28
|
+
# Check cache
|
|
29
|
+
cached = check_cache(ImageEditResult) if cache_enabled?
|
|
30
|
+
return cached if cached
|
|
31
|
+
|
|
32
|
+
# Edit image(s)
|
|
33
|
+
edited_images = edit_images
|
|
34
|
+
|
|
35
|
+
# Build result
|
|
36
|
+
result = build_result(
|
|
37
|
+
images: edited_images,
|
|
38
|
+
started_at: started_at,
|
|
39
|
+
completed_at: Time.current
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
# Cache result
|
|
43
|
+
write_cache(result) if cache_enabled?
|
|
44
|
+
|
|
45
|
+
# Track execution
|
|
46
|
+
record_execution(result) if execution_tracking_enabled?
|
|
47
|
+
|
|
48
|
+
result
|
|
49
|
+
rescue StandardError => e
|
|
50
|
+
record_failed_execution(e, started_at) if execution_tracking_enabled?
|
|
51
|
+
build_error_result(e, started_at)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
private
|
|
55
|
+
|
|
56
|
+
def execution_type
|
|
57
|
+
"image_edit"
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def validate_inputs!
|
|
61
|
+
raise ArgumentError, "Image cannot be blank" if image.nil?
|
|
62
|
+
raise ArgumentError, "Mask cannot be blank" if mask.nil?
|
|
63
|
+
raise ArgumentError, "Prompt cannot be blank" if prompt.nil? || prompt.strip.empty?
|
|
64
|
+
|
|
65
|
+
# Validate image exists if it's a path
|
|
66
|
+
validate_file_exists!(image, "Image")
|
|
67
|
+
validate_file_exists!(mask, "Mask")
|
|
68
|
+
|
|
69
|
+
# Validate prompt length
|
|
70
|
+
max_length = config.max_image_prompt_length || 4000
|
|
71
|
+
if prompt.length > max_length
|
|
72
|
+
raise ArgumentError, "Prompt exceeds maximum length of #{max_length} characters"
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def validate_file_exists!(file, name)
|
|
77
|
+
return unless file.is_a?(String) && !file.start_with?("http")
|
|
78
|
+
|
|
79
|
+
unless File.exist?(file)
|
|
80
|
+
raise ArgumentError, "#{name} file does not exist: #{file}"
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def validate_content_policy!
|
|
85
|
+
policy = self.class.content_policy
|
|
86
|
+
return if policy == :none || policy == :standard
|
|
87
|
+
|
|
88
|
+
ImageGenerator::ContentPolicy.validate!(prompt, policy)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def edit_images
|
|
92
|
+
count = resolve_count
|
|
93
|
+
|
|
94
|
+
Array.new(count) do
|
|
95
|
+
edit_single_image
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def edit_single_image
|
|
100
|
+
# Use RubyLLM's edit endpoint if available
|
|
101
|
+
if RubyLLM.respond_to?(:edit_image)
|
|
102
|
+
RubyLLM.edit_image(
|
|
103
|
+
image: image,
|
|
104
|
+
mask: mask,
|
|
105
|
+
prompt: prompt,
|
|
106
|
+
model: resolve_model,
|
|
107
|
+
size: resolve_size,
|
|
108
|
+
**build_edit_options
|
|
109
|
+
)
|
|
110
|
+
else
|
|
111
|
+
# Fallback: Some providers may use paint with mask support
|
|
112
|
+
RubyLLM.paint(
|
|
113
|
+
prompt,
|
|
114
|
+
model: resolve_model,
|
|
115
|
+
size: resolve_size,
|
|
116
|
+
image: image,
|
|
117
|
+
mask: mask,
|
|
118
|
+
**build_edit_options
|
|
119
|
+
)
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def build_edit_options
|
|
124
|
+
opts = {}
|
|
125
|
+
opts[:assume_model_exists] = true if options[:assume_model_exists]
|
|
126
|
+
opts
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def build_result(images:, started_at:, completed_at:)
|
|
130
|
+
ImageEditResult.new(
|
|
131
|
+
images: images,
|
|
132
|
+
source_image: image,
|
|
133
|
+
mask: mask,
|
|
134
|
+
prompt: prompt,
|
|
135
|
+
model_id: resolve_model,
|
|
136
|
+
size: resolve_size,
|
|
137
|
+
started_at: started_at,
|
|
138
|
+
completed_at: completed_at,
|
|
139
|
+
tenant_id: @tenant_id,
|
|
140
|
+
editor_class: self.class.name
|
|
141
|
+
)
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def build_error_result(error, started_at)
|
|
145
|
+
ImageEditResult.new(
|
|
146
|
+
images: [],
|
|
147
|
+
source_image: image,
|
|
148
|
+
mask: mask,
|
|
149
|
+
prompt: prompt,
|
|
150
|
+
model_id: resolve_model,
|
|
151
|
+
size: resolve_size,
|
|
152
|
+
started_at: started_at,
|
|
153
|
+
completed_at: Time.current,
|
|
154
|
+
tenant_id: @tenant_id,
|
|
155
|
+
editor_class: self.class.name,
|
|
156
|
+
error_class: error.class.name,
|
|
157
|
+
error_message: error.message
|
|
158
|
+
)
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# Resolution methods
|
|
162
|
+
|
|
163
|
+
def resolve_size
|
|
164
|
+
options[:size] || self.class.size
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def resolve_count
|
|
168
|
+
options[:count] || 1
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
# Cache key components
|
|
172
|
+
def cache_key_components
|
|
173
|
+
[
|
|
174
|
+
"image_editor",
|
|
175
|
+
self.class.name,
|
|
176
|
+
self.class.version,
|
|
177
|
+
resolve_model,
|
|
178
|
+
resolve_size,
|
|
179
|
+
Digest::SHA256.hexdigest(prompt),
|
|
180
|
+
Digest::SHA256.hexdigest(image_digest(image)),
|
|
181
|
+
Digest::SHA256.hexdigest(image_digest(mask))
|
|
182
|
+
]
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def image_digest(file)
|
|
186
|
+
if file.is_a?(String) && File.exist?(file)
|
|
187
|
+
File.read(file)
|
|
188
|
+
elsif file.respond_to?(:read)
|
|
189
|
+
content = file.read
|
|
190
|
+
file.rewind if file.respond_to?(:rewind)
|
|
191
|
+
content
|
|
192
|
+
else
|
|
193
|
+
file.to_s
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
def build_execution_metadata(result)
|
|
198
|
+
{
|
|
199
|
+
count: result.count,
|
|
200
|
+
size: result.size,
|
|
201
|
+
prompt_length: prompt.length
|
|
202
|
+
}
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
end
|
|
206
|
+
end
|
|
207
|
+
end
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "editor/dsl"
|
|
4
|
+
require_relative "editor/execution"
|
|
5
|
+
|
|
6
|
+
module RubyLLM
|
|
7
|
+
module Agents
|
|
8
|
+
# Image editor for inpainting and image editing
|
|
9
|
+
#
|
|
10
|
+
# Allows editing specific regions of an image using a mask.
|
|
11
|
+
# The mask indicates which parts of the image should be modified.
|
|
12
|
+
# White areas in the mask are edited, black areas are preserved.
|
|
13
|
+
#
|
|
14
|
+
# @example Basic usage
|
|
15
|
+
# result = RubyLLM::Agents::ImageEditor.call(
|
|
16
|
+
# image: "path/to/image.png",
|
|
17
|
+
# mask: "path/to/mask.png",
|
|
18
|
+
# prompt: "Replace with a red car"
|
|
19
|
+
# )
|
|
20
|
+
# result.url # => "https://..."
|
|
21
|
+
#
|
|
22
|
+
# @example Custom editor class
|
|
23
|
+
# class ProductEditor < RubyLLM::Agents::ImageEditor
|
|
24
|
+
# model "gpt-image-1"
|
|
25
|
+
# size "1024x1024"
|
|
26
|
+
#
|
|
27
|
+
# description "Edits product images"
|
|
28
|
+
# end
|
|
29
|
+
#
|
|
30
|
+
# result = ProductEditor.call(
|
|
31
|
+
# image: product_photo,
|
|
32
|
+
# mask: background_mask,
|
|
33
|
+
# prompt: "Professional studio background"
|
|
34
|
+
# )
|
|
35
|
+
#
|
|
36
|
+
class ImageEditor
|
|
37
|
+
extend DSL
|
|
38
|
+
include Execution
|
|
39
|
+
|
|
40
|
+
class << self
|
|
41
|
+
# Execute image editing with the given source image, mask, and prompt
|
|
42
|
+
#
|
|
43
|
+
# @param image [String, IO] Path, URL, or IO object of the source image
|
|
44
|
+
# @param mask [String, IO] Path, URL, or IO object of the mask image
|
|
45
|
+
# @param prompt [String] Description of the desired edit
|
|
46
|
+
# @param options [Hash] Additional options (model, size, etc.)
|
|
47
|
+
# @return [ImageEditResult] The result containing edited image
|
|
48
|
+
def call(image:, mask:, prompt:, **options)
|
|
49
|
+
new(image: image, mask: mask, prompt: prompt, **options).call
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Ensure subclasses inherit DSL settings
|
|
53
|
+
def inherited(subclass)
|
|
54
|
+
super
|
|
55
|
+
subclass.instance_variable_set(:@model, @model)
|
|
56
|
+
subclass.instance_variable_set(:@size, @size)
|
|
57
|
+
subclass.instance_variable_set(:@version, @version)
|
|
58
|
+
subclass.instance_variable_set(:@description, @description)
|
|
59
|
+
subclass.instance_variable_set(:@cache_ttl, @cache_ttl)
|
|
60
|
+
subclass.instance_variable_set(:@content_policy, @content_policy)
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
attr_reader :image, :mask, :prompt, :options, :tenant_id
|
|
65
|
+
|
|
66
|
+
# Initialize a new image editor instance
|
|
67
|
+
#
|
|
68
|
+
# @param image [String, IO] Source image (path, URL, or IO object)
|
|
69
|
+
# @param mask [String, IO] Mask image (path, URL, or IO object)
|
|
70
|
+
# @param prompt [String] Description of the desired edit
|
|
71
|
+
# @param options [Hash] Additional options
|
|
72
|
+
# @option options [String] :model Model to use
|
|
73
|
+
# @option options [String] :size Output image size
|
|
74
|
+
# @option options [Integer] :count Number of edits to generate
|
|
75
|
+
# @option options [Object] :tenant Tenant for multi-tenancy
|
|
76
|
+
def initialize(image:, mask:, prompt:, **options)
|
|
77
|
+
@image = image
|
|
78
|
+
@mask = mask
|
|
79
|
+
@prompt = prompt
|
|
80
|
+
@options = options
|
|
81
|
+
@tenant_id = nil
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Execute the image edit
|
|
85
|
+
#
|
|
86
|
+
# @return [ImageEditResult] The result containing edited image
|
|
87
|
+
def call
|
|
88
|
+
execute
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
module Agents
|
|
5
|
+
class ImageGenerator
|
|
6
|
+
# ActiveStorage integration for image generators
|
|
7
|
+
#
|
|
8
|
+
# Provides convenience methods for generating images and directly
|
|
9
|
+
# attaching them to ActiveStorage attachments.
|
|
10
|
+
#
|
|
11
|
+
# @example Attaching to a model
|
|
12
|
+
# class Product < ApplicationRecord
|
|
13
|
+
# has_one_attached :hero_image
|
|
14
|
+
# end
|
|
15
|
+
#
|
|
16
|
+
# class ProductImageGenerator < RubyLLM::Agents::ImageGenerator
|
|
17
|
+
# include RubyLLM::Agents::ImageGenerator::ActiveStorageSupport
|
|
18
|
+
#
|
|
19
|
+
# model "gpt-image-1"
|
|
20
|
+
# size "1024x1024"
|
|
21
|
+
# end
|
|
22
|
+
#
|
|
23
|
+
# product = Product.find(1)
|
|
24
|
+
# result = ProductImageGenerator.generate_and_attach(
|
|
25
|
+
# prompt: "Professional product photo of a red sneaker",
|
|
26
|
+
# record: product,
|
|
27
|
+
# attachment_name: :hero_image
|
|
28
|
+
# )
|
|
29
|
+
#
|
|
30
|
+
module ActiveStorageSupport
|
|
31
|
+
extend ActiveSupport::Concern
|
|
32
|
+
|
|
33
|
+
class_methods do
|
|
34
|
+
# Generate an image and attach it to a record
|
|
35
|
+
#
|
|
36
|
+
# @param prompt [String] The generation prompt
|
|
37
|
+
# @param record [ActiveRecord::Base] The record to attach to
|
|
38
|
+
# @param attachment_name [Symbol] Name of the attachment (e.g., :avatar)
|
|
39
|
+
# @param options [Hash] Additional options for generation
|
|
40
|
+
# @return [ImageGenerationResult] The generation result
|
|
41
|
+
def generate_and_attach(prompt:, record:, attachment_name:, **options)
|
|
42
|
+
result = call(prompt: prompt, **options)
|
|
43
|
+
|
|
44
|
+
return result unless result.success?
|
|
45
|
+
|
|
46
|
+
attach_result_to_record(result, record, attachment_name, options)
|
|
47
|
+
|
|
48
|
+
result
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Generate multiple images and attach them to a has_many_attached
|
|
52
|
+
#
|
|
53
|
+
# @param prompt [String] The generation prompt
|
|
54
|
+
# @param record [ActiveRecord::Base] The record to attach to
|
|
55
|
+
# @param attachment_name [Symbol] Name of the attachment (e.g., :photos)
|
|
56
|
+
# @param count [Integer] Number of images to generate
|
|
57
|
+
# @param options [Hash] Additional options for generation
|
|
58
|
+
# @return [ImageGenerationResult] The generation result
|
|
59
|
+
def generate_and_attach_multiple(prompt:, record:, attachment_name:, count: 1, **options)
|
|
60
|
+
result = call(prompt: prompt, count: count, **options)
|
|
61
|
+
|
|
62
|
+
return result unless result.success?
|
|
63
|
+
|
|
64
|
+
attachment = record.public_send(attachment_name)
|
|
65
|
+
|
|
66
|
+
result.images.each_with_index do |image, idx|
|
|
67
|
+
attach_image_to_collection(image, attachment, idx, result)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
result
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
private
|
|
74
|
+
|
|
75
|
+
def attach_result_to_record(result, record, attachment_name, options)
|
|
76
|
+
attachment = record.public_send(attachment_name)
|
|
77
|
+
filename = options[:filename] || generate_filename
|
|
78
|
+
|
|
79
|
+
if result.base64?
|
|
80
|
+
attachment.attach(
|
|
81
|
+
io: StringIO.new(result.to_blob),
|
|
82
|
+
filename: filename,
|
|
83
|
+
content_type: result.mime_type || "image/png"
|
|
84
|
+
)
|
|
85
|
+
else
|
|
86
|
+
attach_from_url(attachment, result.url, filename)
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def attach_image_to_collection(image, attachment, index, result)
|
|
91
|
+
filename = generate_filename(index)
|
|
92
|
+
|
|
93
|
+
if image.respond_to?(:data) && image.data
|
|
94
|
+
attachment.attach(
|
|
95
|
+
io: StringIO.new(image.to_blob),
|
|
96
|
+
filename: filename,
|
|
97
|
+
content_type: image.mime_type || "image/png"
|
|
98
|
+
)
|
|
99
|
+
elsif image.respond_to?(:url) && image.url
|
|
100
|
+
attach_from_url(attachment, image.url, filename)
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def attach_from_url(attachment, url, filename)
|
|
105
|
+
require "open-uri"
|
|
106
|
+
|
|
107
|
+
downloaded = URI.parse(url).open
|
|
108
|
+
attachment.attach(
|
|
109
|
+
io: downloaded,
|
|
110
|
+
filename: filename,
|
|
111
|
+
content_type: "image/png"
|
|
112
|
+
)
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def generate_filename(index = nil)
|
|
116
|
+
timestamp = Time.current.to_i
|
|
117
|
+
if index
|
|
118
|
+
"generated_#{timestamp}_#{index + 1}.png"
|
|
119
|
+
else
|
|
120
|
+
"generated_#{timestamp}.png"
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
end
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
module Agents
|
|
5
|
+
class ImageGenerator
|
|
6
|
+
# Content policy enforcement for image generation prompts
|
|
7
|
+
#
|
|
8
|
+
# Validates prompts against configurable policy levels to prevent
|
|
9
|
+
# generation of inappropriate content.
|
|
10
|
+
#
|
|
11
|
+
# @example Using content policy in a generator
|
|
12
|
+
# class SafeImageGenerator < RubyLLM::Agents::ImageGenerator
|
|
13
|
+
# content_policy :strict
|
|
14
|
+
# end
|
|
15
|
+
#
|
|
16
|
+
# @example Manual validation
|
|
17
|
+
# ContentPolicy.validate!("A beautiful sunset", :moderate)
|
|
18
|
+
# # => nil (passes)
|
|
19
|
+
#
|
|
20
|
+
# ContentPolicy.validate!("Violent scene", :strict)
|
|
21
|
+
# # => raises ContentPolicyViolation
|
|
22
|
+
#
|
|
23
|
+
module ContentPolicy
|
|
24
|
+
# Blocked patterns by policy level
|
|
25
|
+
#
|
|
26
|
+
# :strict - Blocks violence, nudity, hate, weapons, drugs
|
|
27
|
+
# :moderate - Blocks explicit content, gore, hate speech
|
|
28
|
+
# :standard - No blocking (relies on model's built-in filters)
|
|
29
|
+
# :none - No validation at all
|
|
30
|
+
#
|
|
31
|
+
BLOCKED_PATTERNS = {
|
|
32
|
+
strict: [
|
|
33
|
+
/\b(violence|violent|gore|blood|death|kill|murder)\b/i,
|
|
34
|
+
/\b(nude|naked|nsfw|explicit|sexual|porn)\b/i,
|
|
35
|
+
/\b(hate|racist|discrimination|slur)\b/i,
|
|
36
|
+
/\b(weapon|gun|knife|bomb|explosive)\b/i,
|
|
37
|
+
/\b(drug|cocaine|heroin|meth)\b/i
|
|
38
|
+
],
|
|
39
|
+
moderate: [
|
|
40
|
+
/\b(nude|naked|nsfw|explicit|sexual|porn)\b/i,
|
|
41
|
+
/\b(gore|graphic.?violence)\b/i,
|
|
42
|
+
/\b(hate.?speech|slur)\b/i
|
|
43
|
+
],
|
|
44
|
+
standard: [],
|
|
45
|
+
none: []
|
|
46
|
+
}.freeze
|
|
47
|
+
|
|
48
|
+
class << self
|
|
49
|
+
# Validate a prompt against a policy level
|
|
50
|
+
#
|
|
51
|
+
# @param prompt [String] The prompt to validate
|
|
52
|
+
# @param level [Symbol] Policy level (:none, :standard, :moderate, :strict)
|
|
53
|
+
# @raise [ContentPolicyViolation] If prompt violates the policy
|
|
54
|
+
def validate!(prompt, level)
|
|
55
|
+
return if level == :none || level.nil?
|
|
56
|
+
|
|
57
|
+
patterns = BLOCKED_PATTERNS[level.to_sym] || BLOCKED_PATTERNS[:standard]
|
|
58
|
+
|
|
59
|
+
patterns.each do |pattern|
|
|
60
|
+
if prompt.match?(pattern)
|
|
61
|
+
raise ContentPolicyViolation,
|
|
62
|
+
"Prompt contains content blocked by #{level} policy"
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Check if a prompt passes the policy (non-raising version)
|
|
68
|
+
#
|
|
69
|
+
# @param prompt [String] The prompt to check
|
|
70
|
+
# @param level [Symbol] Policy level
|
|
71
|
+
# @return [Boolean] true if prompt passes
|
|
72
|
+
def valid?(prompt, level)
|
|
73
|
+
validate!(prompt, level)
|
|
74
|
+
true
|
|
75
|
+
rescue ContentPolicyViolation
|
|
76
|
+
false
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Get the matched pattern for a violation (for debugging)
|
|
80
|
+
#
|
|
81
|
+
# @param prompt [String] The prompt to check
|
|
82
|
+
# @param level [Symbol] Policy level
|
|
83
|
+
# @return [Regexp, nil] The matched pattern or nil
|
|
84
|
+
def matched_pattern(prompt, level)
|
|
85
|
+
patterns = BLOCKED_PATTERNS[level.to_sym] || []
|
|
86
|
+
patterns.find { |pattern| prompt.match?(pattern) }
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Exception raised when a prompt violates content policy
|
|
92
|
+
class ContentPolicyViolation < StandardError; end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|