ruby_llm-agents 0.4.0 → 1.0.0.beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (208) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +225 -34
  3. data/app/controllers/ruby_llm/agents/agents_controller.rb +136 -16
  4. data/app/controllers/ruby_llm/agents/api_configurations_controller.rb +214 -0
  5. data/app/controllers/ruby_llm/agents/dashboard_controller.rb +29 -9
  6. data/app/controllers/ruby_llm/agents/{settings_controller.rb → system_config_controller.rb} +3 -3
  7. data/app/controllers/ruby_llm/agents/tenants_controller.rb +109 -0
  8. data/app/controllers/ruby_llm/agents/workflows_controller.rb +355 -0
  9. data/app/helpers/ruby_llm/agents/application_helper.rb +25 -0
  10. data/app/models/ruby_llm/agents/api_configuration.rb +386 -0
  11. data/app/models/ruby_llm/agents/execution.rb +3 -0
  12. data/app/models/ruby_llm/agents/tenant_budget.rb +112 -14
  13. data/app/services/ruby_llm/agents/agent_registry.rb +51 -12
  14. data/app/views/layouts/ruby_llm/agents/application.html.erb +5 -30
  15. data/app/views/ruby_llm/agents/agents/_agent.html.erb +13 -1
  16. data/app/views/ruby_llm/agents/agents/_config_agent.html.erb +235 -0
  17. data/app/views/ruby_llm/agents/agents/_config_embedder.html.erb +70 -0
  18. data/app/views/ruby_llm/agents/agents/_config_image_generator.html.erb +152 -0
  19. data/app/views/ruby_llm/agents/agents/_config_moderator.html.erb +63 -0
  20. data/app/views/ruby_llm/agents/agents/_config_speaker.html.erb +108 -0
  21. data/app/views/ruby_llm/agents/agents/_config_transcriber.html.erb +91 -0
  22. data/app/views/ruby_llm/agents/agents/_workflow.html.erb +1 -1
  23. data/app/views/ruby_llm/agents/agents/index.html.erb +74 -9
  24. data/app/views/ruby_llm/agents/agents/show.html.erb +18 -378
  25. data/app/views/ruby_llm/agents/api_configurations/_api_key_field.html.erb +34 -0
  26. data/app/views/ruby_llm/agents/api_configurations/_form.html.erb +288 -0
  27. data/app/views/ruby_llm/agents/api_configurations/edit.html.erb +95 -0
  28. data/app/views/ruby_llm/agents/api_configurations/edit_tenant.html.erb +97 -0
  29. data/app/views/ruby_llm/agents/api_configurations/show.html.erb +211 -0
  30. data/app/views/ruby_llm/agents/api_configurations/tenant.html.erb +179 -0
  31. data/app/views/ruby_llm/agents/dashboard/_action_center.html.erb +1 -1
  32. data/app/views/ruby_llm/agents/dashboard/_agent_comparison.html.erb +269 -15
  33. data/app/views/ruby_llm/agents/executions/show.html.erb +98 -0
  34. data/app/views/ruby_llm/agents/shared/_agent_type_badge.html.erb +93 -0
  35. data/app/views/ruby_llm/agents/{settings → system_config}/show.html.erb +1 -1
  36. data/app/views/ruby_llm/agents/tenants/_form.html.erb +150 -0
  37. data/app/views/ruby_llm/agents/tenants/edit.html.erb +13 -0
  38. data/app/views/ruby_llm/agents/tenants/index.html.erb +129 -0
  39. data/app/views/ruby_llm/agents/tenants/show.html.erb +374 -0
  40. data/app/views/ruby_llm/agents/workflows/_step_performance.html.erb +236 -0
  41. data/app/views/ruby_llm/agents/workflows/_structure_parallel.html.erb +76 -0
  42. data/app/views/ruby_llm/agents/workflows/_structure_pipeline.html.erb +74 -0
  43. data/app/views/ruby_llm/agents/workflows/_structure_router.html.erb +108 -0
  44. data/app/views/ruby_llm/agents/workflows/show.html.erb +442 -0
  45. data/config/routes.rb +13 -1
  46. data/lib/generators/ruby_llm_agents/agent_generator.rb +56 -7
  47. data/lib/generators/ruby_llm_agents/api_configuration_generator.rb +100 -0
  48. data/lib/generators/ruby_llm_agents/background_remover_generator.rb +110 -0
  49. data/lib/generators/ruby_llm_agents/embedder_generator.rb +107 -0
  50. data/lib/generators/ruby_llm_agents/image_analyzer_generator.rb +115 -0
  51. data/lib/generators/ruby_llm_agents/image_editor_generator.rb +108 -0
  52. data/lib/generators/ruby_llm_agents/image_generator_generator.rb +116 -0
  53. data/lib/generators/ruby_llm_agents/image_pipeline_generator.rb +178 -0
  54. data/lib/generators/ruby_llm_agents/image_transformer_generator.rb +109 -0
  55. data/lib/generators/ruby_llm_agents/image_upscaler_generator.rb +103 -0
  56. data/lib/generators/ruby_llm_agents/image_variator_generator.rb +102 -0
  57. data/lib/generators/ruby_llm_agents/install_generator.rb +76 -4
  58. data/lib/generators/ruby_llm_agents/restructure_generator.rb +292 -0
  59. data/lib/generators/ruby_llm_agents/speaker_generator.rb +121 -0
  60. data/lib/generators/ruby_llm_agents/templates/add_execution_type_migration.rb.tt +8 -0
  61. data/lib/generators/ruby_llm_agents/templates/agent.rb.tt +99 -84
  62. data/lib/generators/ruby_llm_agents/templates/application_agent.rb.tt +42 -40
  63. data/lib/generators/ruby_llm_agents/templates/application_background_remover.rb.tt +26 -0
  64. data/lib/generators/ruby_llm_agents/templates/application_embedder.rb.tt +50 -0
  65. data/lib/generators/ruby_llm_agents/templates/application_image_analyzer.rb.tt +26 -0
  66. data/lib/generators/ruby_llm_agents/templates/application_image_editor.rb.tt +20 -0
  67. data/lib/generators/ruby_llm_agents/templates/application_image_generator.rb.tt +38 -0
  68. data/lib/generators/ruby_llm_agents/templates/application_image_pipeline.rb.tt +139 -0
  69. data/lib/generators/ruby_llm_agents/templates/application_image_transformer.rb.tt +21 -0
  70. data/lib/generators/ruby_llm_agents/templates/application_image_upscaler.rb.tt +20 -0
  71. data/lib/generators/ruby_llm_agents/templates/application_image_variator.rb.tt +20 -0
  72. data/lib/generators/ruby_llm_agents/templates/application_speaker.rb.tt +49 -0
  73. data/lib/generators/ruby_llm_agents/templates/application_transcriber.rb.tt +53 -0
  74. data/lib/generators/ruby_llm_agents/templates/background_remover.rb.tt +44 -0
  75. data/lib/generators/ruby_llm_agents/templates/create_api_configurations_migration.rb.tt +90 -0
  76. data/lib/generators/ruby_llm_agents/templates/embedder.rb.tt +41 -0
  77. data/lib/generators/ruby_llm_agents/templates/image_analyzer.rb.tt +45 -0
  78. data/lib/generators/ruby_llm_agents/templates/image_editor.rb.tt +35 -0
  79. data/lib/generators/ruby_llm_agents/templates/image_generator.rb.tt +47 -0
  80. data/lib/generators/ruby_llm_agents/templates/image_pipeline.rb.tt +50 -0
  81. data/lib/generators/ruby_llm_agents/templates/image_transformer.rb.tt +44 -0
  82. data/lib/generators/ruby_llm_agents/templates/image_upscaler.rb.tt +38 -0
  83. data/lib/generators/ruby_llm_agents/templates/image_variator.rb.tt +33 -0
  84. data/lib/generators/ruby_llm_agents/templates/skills/AGENTS.md.tt +228 -0
  85. data/lib/generators/ruby_llm_agents/templates/skills/BACKGROUND_REMOVERS.md.tt +131 -0
  86. data/lib/generators/ruby_llm_agents/templates/skills/EMBEDDERS.md.tt +255 -0
  87. data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_ANALYZERS.md.tt +120 -0
  88. data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_EDITORS.md.tt +102 -0
  89. data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_GENERATORS.md.tt +282 -0
  90. data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_PIPELINES.md.tt +228 -0
  91. data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_TRANSFORMERS.md.tt +120 -0
  92. data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_UPSCALERS.md.tt +110 -0
  93. data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_VARIATORS.md.tt +120 -0
  94. data/lib/generators/ruby_llm_agents/templates/skills/SPEAKERS.md.tt +212 -0
  95. data/lib/generators/ruby_llm_agents/templates/skills/TOOLS.md.tt +227 -0
  96. data/lib/generators/ruby_llm_agents/templates/skills/TRANSCRIBERS.md.tt +251 -0
  97. data/lib/generators/ruby_llm_agents/templates/skills/WORKFLOWS.md.tt +300 -0
  98. data/lib/generators/ruby_llm_agents/templates/speaker.rb.tt +56 -0
  99. data/lib/generators/ruby_llm_agents/templates/transcriber.rb.tt +51 -0
  100. data/lib/generators/ruby_llm_agents/transcriber_generator.rb +107 -0
  101. data/lib/generators/ruby_llm_agents/upgrade_generator.rb +152 -1
  102. data/lib/ruby_llm/agents/audio/speaker.rb +553 -0
  103. data/lib/ruby_llm/agents/audio/transcriber.rb +669 -0
  104. data/lib/ruby_llm/agents/base_agent.rb +675 -0
  105. data/lib/ruby_llm/agents/core/base/moderation_dsl.rb +181 -0
  106. data/lib/ruby_llm/agents/core/base/moderation_execution.rb +274 -0
  107. data/lib/ruby_llm/agents/core/base.rb +135 -0
  108. data/lib/ruby_llm/agents/core/configuration.rb +981 -0
  109. data/lib/ruby_llm/agents/core/errors.rb +150 -0
  110. data/lib/ruby_llm/agents/{instrumentation.rb → core/instrumentation.rb} +93 -4
  111. data/lib/ruby_llm/agents/core/llm_tenant.rb +358 -0
  112. data/lib/ruby_llm/agents/core/resolved_config.rb +348 -0
  113. data/lib/ruby_llm/agents/{version.rb → core/version.rb} +1 -1
  114. data/lib/ruby_llm/agents/dsl/base.rb +110 -0
  115. data/lib/ruby_llm/agents/dsl/caching.rb +142 -0
  116. data/lib/ruby_llm/agents/dsl/reliability.rb +307 -0
  117. data/lib/ruby_llm/agents/dsl.rb +41 -0
  118. data/lib/ruby_llm/agents/image/analyzer/dsl.rb +130 -0
  119. data/lib/ruby_llm/agents/image/analyzer/execution.rb +402 -0
  120. data/lib/ruby_llm/agents/image/analyzer.rb +90 -0
  121. data/lib/ruby_llm/agents/image/background_remover/dsl.rb +154 -0
  122. data/lib/ruby_llm/agents/image/background_remover/execution.rb +240 -0
  123. data/lib/ruby_llm/agents/image/background_remover.rb +89 -0
  124. data/lib/ruby_llm/agents/image/concerns/image_operation_dsl.rb +91 -0
  125. data/lib/ruby_llm/agents/image/concerns/image_operation_execution.rb +165 -0
  126. data/lib/ruby_llm/agents/image/editor/dsl.rb +56 -0
  127. data/lib/ruby_llm/agents/image/editor/execution.rb +207 -0
  128. data/lib/ruby_llm/agents/image/editor.rb +92 -0
  129. data/lib/ruby_llm/agents/image/generator/active_storage_support.rb +127 -0
  130. data/lib/ruby_llm/agents/image/generator/content_policy.rb +95 -0
  131. data/lib/ruby_llm/agents/image/generator/pricing.rb +353 -0
  132. data/lib/ruby_llm/agents/image/generator/templates.rb +124 -0
  133. data/lib/ruby_llm/agents/image/generator.rb +455 -0
  134. data/lib/ruby_llm/agents/image/pipeline/dsl.rb +213 -0
  135. data/lib/ruby_llm/agents/image/pipeline/execution.rb +382 -0
  136. data/lib/ruby_llm/agents/image/pipeline.rb +97 -0
  137. data/lib/ruby_llm/agents/image/transformer/dsl.rb +148 -0
  138. data/lib/ruby_llm/agents/image/transformer/execution.rb +223 -0
  139. data/lib/ruby_llm/agents/image/transformer.rb +95 -0
  140. data/lib/ruby_llm/agents/image/upscaler/dsl.rb +83 -0
  141. data/lib/ruby_llm/agents/image/upscaler/execution.rb +219 -0
  142. data/lib/ruby_llm/agents/image/upscaler.rb +81 -0
  143. data/lib/ruby_llm/agents/image/variator/dsl.rb +62 -0
  144. data/lib/ruby_llm/agents/image/variator/execution.rb +189 -0
  145. data/lib/ruby_llm/agents/image/variator.rb +80 -0
  146. data/lib/ruby_llm/agents/{alert_manager.rb → infrastructure/alert_manager.rb} +17 -22
  147. data/lib/ruby_llm/agents/infrastructure/budget/budget_query.rb +145 -0
  148. data/lib/ruby_llm/agents/infrastructure/budget/config_resolver.rb +149 -0
  149. data/lib/ruby_llm/agents/infrastructure/budget/forecaster.rb +68 -0
  150. data/lib/ruby_llm/agents/infrastructure/budget/spend_recorder.rb +279 -0
  151. data/lib/ruby_llm/agents/infrastructure/budget_tracker.rb +275 -0
  152. data/lib/ruby_llm/agents/{execution_logger_job.rb → infrastructure/execution_logger_job.rb} +17 -1
  153. data/lib/ruby_llm/agents/{reliability → infrastructure/reliability}/executor.rb +2 -1
  154. data/lib/ruby_llm/agents/{reliability → infrastructure/reliability}/retry_strategy.rb +9 -3
  155. data/lib/ruby_llm/agents/{reliability.rb → infrastructure/reliability.rb} +11 -21
  156. data/lib/ruby_llm/agents/pipeline/builder.rb +215 -0
  157. data/lib/ruby_llm/agents/pipeline/context.rb +255 -0
  158. data/lib/ruby_llm/agents/pipeline/executor.rb +86 -0
  159. data/lib/ruby_llm/agents/pipeline/middleware/base.rb +124 -0
  160. data/lib/ruby_llm/agents/pipeline/middleware/budget.rb +95 -0
  161. data/lib/ruby_llm/agents/pipeline/middleware/cache.rb +171 -0
  162. data/lib/ruby_llm/agents/pipeline/middleware/instrumentation.rb +415 -0
  163. data/lib/ruby_llm/agents/pipeline/middleware/reliability.rb +276 -0
  164. data/lib/ruby_llm/agents/pipeline/middleware/tenant.rb +196 -0
  165. data/lib/ruby_llm/agents/pipeline.rb +68 -0
  166. data/lib/ruby_llm/agents/{engine.rb → rails/engine.rb} +79 -10
  167. data/lib/ruby_llm/agents/results/background_removal_result.rb +286 -0
  168. data/lib/ruby_llm/agents/{result.rb → results/base.rb} +73 -1
  169. data/lib/ruby_llm/agents/results/embedding_result.rb +243 -0
  170. data/lib/ruby_llm/agents/results/image_analysis_result.rb +314 -0
  171. data/lib/ruby_llm/agents/results/image_edit_result.rb +250 -0
  172. data/lib/ruby_llm/agents/results/image_generation_result.rb +346 -0
  173. data/lib/ruby_llm/agents/results/image_pipeline_result.rb +399 -0
  174. data/lib/ruby_llm/agents/results/image_transform_result.rb +251 -0
  175. data/lib/ruby_llm/agents/results/image_upscale_result.rb +255 -0
  176. data/lib/ruby_llm/agents/results/image_variation_result.rb +237 -0
  177. data/lib/ruby_llm/agents/results/moderation_result.rb +158 -0
  178. data/lib/ruby_llm/agents/results/speech_result.rb +338 -0
  179. data/lib/ruby_llm/agents/results/transcription_result.rb +408 -0
  180. data/lib/ruby_llm/agents/text/embedder.rb +444 -0
  181. data/lib/ruby_llm/agents/text/moderator.rb +237 -0
  182. data/lib/ruby_llm/agents/workflow/async.rb +220 -0
  183. data/lib/ruby_llm/agents/workflow/async_executor.rb +156 -0
  184. data/lib/ruby_llm/agents/{workflow.rb → workflow/orchestrator.rb} +6 -5
  185. data/lib/ruby_llm/agents/workflow/parallel.rb +34 -17
  186. data/lib/ruby_llm/agents/workflow/thread_pool.rb +185 -0
  187. data/lib/ruby_llm/agents.rb +86 -20
  188. metadata +189 -35
  189. data/lib/ruby_llm/agents/base/caching.rb +0 -40
  190. data/lib/ruby_llm/agents/base/cost_calculation.rb +0 -105
  191. data/lib/ruby_llm/agents/base/dsl.rb +0 -324
  192. data/lib/ruby_llm/agents/base/execution.rb +0 -283
  193. data/lib/ruby_llm/agents/base/reliability_dsl.rb +0 -82
  194. data/lib/ruby_llm/agents/base/reliability_execution.rb +0 -136
  195. data/lib/ruby_llm/agents/base/response_building.rb +0 -86
  196. data/lib/ruby_llm/agents/base/tool_tracking.rb +0 -57
  197. data/lib/ruby_llm/agents/base.rb +0 -209
  198. data/lib/ruby_llm/agents/budget_tracker.rb +0 -471
  199. data/lib/ruby_llm/agents/configuration.rb +0 -357
  200. /data/lib/ruby_llm/agents/{deprecations.rb → core/deprecations.rb} +0 -0
  201. /data/lib/ruby_llm/agents/{inflections.rb → core/inflections.rb} +0 -0
  202. /data/lib/ruby_llm/agents/{attempt_tracker.rb → infrastructure/attempt_tracker.rb} +0 -0
  203. /data/lib/ruby_llm/agents/{cache_helper.rb → infrastructure/cache_helper.rb} +0 -0
  204. /data/lib/ruby_llm/agents/{circuit_breaker.rb → infrastructure/circuit_breaker.rb} +0 -0
  205. /data/lib/ruby_llm/agents/{redactor.rb → infrastructure/redactor.rb} +0 -0
  206. /data/lib/ruby_llm/agents/{reliability → infrastructure/reliability}/breaker_manager.rb +0 -0
  207. /data/lib/ruby_llm/agents/{reliability → infrastructure/reliability}/execution_constraints.rb +0 -0
  208. /data/lib/ruby_llm/agents/{reliability → infrastructure/reliability}/fallback_routing.rb +0 -0
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../concerns/image_operation_dsl"
4
+
5
+ module RubyLLM
6
+ module Agents
7
+ class ImageVariator
8
+ # DSL for configuring image variators
9
+ #
10
+ # Provides class-level methods to configure model, size,
11
+ # variation strength, and other image variation parameters.
12
+ #
13
+ # @example
14
+ # class LogoVariator < RubyLLM::Agents::ImageVariator
15
+ # model "gpt-image-1"
16
+ # size "1024x1024"
17
+ # variation_strength 0.3
18
+ # cache_for 1.hour
19
+ # end
20
+ #
21
+ module DSL
22
+ include Concerns::ImageOperationDSL
23
+
24
+ # Set or get the output image size
25
+ #
26
+ # @param value [String, nil] Size (e.g., "1024x1024")
27
+ # @return [String] The size to use
28
+ def size(value = nil)
29
+ if value
30
+ @size = value
31
+ else
32
+ @size || inherited_or_default(:size, config.default_image_size)
33
+ end
34
+ end
35
+
36
+ # Set or get the variation strength
37
+ #
38
+ # Controls how different variations should be from the original.
39
+ # Higher values produce more diverse variations.
40
+ #
41
+ # @param value [Float, nil] Strength (0.0-1.0)
42
+ # @return [Float] The variation strength
43
+ def variation_strength(value = nil)
44
+ if value
45
+ unless value.is_a?(Numeric) && value.between?(0.0, 1.0)
46
+ raise ArgumentError, "Variation strength must be between 0.0 and 1.0"
47
+ end
48
+ @variation_strength = value.to_f
49
+ else
50
+ @variation_strength || inherited_or_default(:variation_strength, 0.5)
51
+ end
52
+ end
53
+
54
+ private
55
+
56
+ def default_model
57
+ config.default_variator_model || config.default_image_model
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,189 @@
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 ImageVariator
9
+ # Execution logic for image variators
10
+ #
11
+ # Handles image validation, budget tracking, caching,
12
+ # variation generation, and result building.
13
+ #
14
+ module Execution
15
+ include Concerns::ImageOperationExecution
16
+
17
+ # Execute the image variation pipeline
18
+ #
19
+ # @return [ImageVariationResult] The result containing variation images
20
+ def execute
21
+ started_at = Time.current
22
+
23
+ resolve_tenant_context!
24
+ check_budget! if budget_tracking_enabled?
25
+ validate_image!
26
+
27
+ # Check cache
28
+ cached = check_cache(ImageVariationResult) if cache_enabled?
29
+ return cached if cached
30
+
31
+ # Generate variations
32
+ variations = generate_variations
33
+
34
+ # Build result
35
+ result = build_result(
36
+ images: variations,
37
+ started_at: started_at,
38
+ completed_at: Time.current
39
+ )
40
+
41
+ # Cache result
42
+ write_cache(result) if cache_enabled?
43
+
44
+ # Track execution
45
+ record_execution(result) if execution_tracking_enabled?
46
+
47
+ result
48
+ rescue StandardError => e
49
+ record_failed_execution(e, started_at) if execution_tracking_enabled?
50
+ build_error_result(e, started_at)
51
+ end
52
+
53
+ private
54
+
55
+ def execution_type
56
+ "image_variation"
57
+ end
58
+
59
+ def validate_image!
60
+ raise ArgumentError, "Image cannot be blank" if image.nil?
61
+
62
+ # Validate image exists if it's a path
63
+ if image.is_a?(String) && !image.start_with?("http")
64
+ unless File.exist?(image)
65
+ raise ArgumentError, "Image file does not exist: #{image}"
66
+ end
67
+ end
68
+ end
69
+
70
+ def generate_variations
71
+ count = resolve_count
72
+
73
+ # Generate variations using the underlying image API
74
+ # Note: The actual implementation depends on the provider
75
+ Array.new(count) do
76
+ generate_single_variation
77
+ end
78
+ end
79
+
80
+ def generate_single_variation
81
+ # Use RubyLLM's variation endpoint if available,
82
+ # otherwise use edit with the original image
83
+ if RubyLLM.respond_to?(:create_image_variation)
84
+ RubyLLM.create_image_variation(
85
+ image: image,
86
+ model: resolve_model,
87
+ size: resolve_size,
88
+ **build_variation_options
89
+ )
90
+ else
91
+ # Fallback: Use paint with image reference
92
+ # This approach works for models that support img2img
93
+ RubyLLM.paint(
94
+ "Create a variation of this image",
95
+ model: resolve_model,
96
+ size: resolve_size,
97
+ reference_image: image,
98
+ strength: resolve_variation_strength,
99
+ **build_variation_options
100
+ )
101
+ end
102
+ end
103
+
104
+ def build_variation_options
105
+ opts = {}
106
+ opts[:assume_model_exists] = true if options[:assume_model_exists]
107
+ opts
108
+ end
109
+
110
+ def build_result(images:, started_at:, completed_at:)
111
+ ImageVariationResult.new(
112
+ images: images,
113
+ source_image: image,
114
+ model_id: resolve_model,
115
+ size: resolve_size,
116
+ variation_strength: resolve_variation_strength,
117
+ started_at: started_at,
118
+ completed_at: completed_at,
119
+ tenant_id: @tenant_id,
120
+ variator_class: self.class.name
121
+ )
122
+ end
123
+
124
+ def build_error_result(error, started_at)
125
+ ImageVariationResult.new(
126
+ images: [],
127
+ source_image: image,
128
+ model_id: resolve_model,
129
+ size: resolve_size,
130
+ variation_strength: resolve_variation_strength,
131
+ started_at: started_at,
132
+ completed_at: Time.current,
133
+ tenant_id: @tenant_id,
134
+ variator_class: self.class.name,
135
+ error_class: error.class.name,
136
+ error_message: error.message
137
+ )
138
+ end
139
+
140
+ # Resolution methods
141
+
142
+ def resolve_size
143
+ options[:size] || self.class.size
144
+ end
145
+
146
+ def resolve_variation_strength
147
+ options[:variation_strength] || self.class.variation_strength
148
+ end
149
+
150
+ def resolve_count
151
+ options[:count] || 1
152
+ end
153
+
154
+ # Cache key components
155
+ def cache_key_components
156
+ [
157
+ "image_variator",
158
+ self.class.name,
159
+ self.class.version,
160
+ resolve_model,
161
+ resolve_size,
162
+ resolve_variation_strength.to_s,
163
+ Digest::SHA256.hexdigest(image_digest)
164
+ ]
165
+ end
166
+
167
+ def image_digest
168
+ if image.is_a?(String) && File.exist?(image)
169
+ File.read(image)
170
+ elsif image.respond_to?(:read)
171
+ content = image.read
172
+ image.rewind if image.respond_to?(:rewind)
173
+ content
174
+ else
175
+ image.to_s
176
+ end
177
+ end
178
+
179
+ def build_execution_metadata(result)
180
+ {
181
+ count: result.count,
182
+ size: result.size,
183
+ variation_strength: result.variation_strength
184
+ }
185
+ end
186
+ end
187
+ end
188
+ end
189
+ end
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "variator/dsl"
4
+ require_relative "variator/execution"
5
+
6
+ module RubyLLM
7
+ module Agents
8
+ # Image variator for generating variations of existing images
9
+ #
10
+ # Creates variations of an input image while maintaining the overall
11
+ # composition and style. Useful for exploring design alternatives
12
+ # or generating A/B test variants.
13
+ #
14
+ # @example Basic usage
15
+ # result = RubyLLM::Agents::ImageVariator.call(image: "path/to/image.png")
16
+ # result.urls # => ["https://...", ...]
17
+ #
18
+ # @example Custom variator class
19
+ # class LogoVariator < RubyLLM::Agents::ImageVariator
20
+ # model "gpt-image-1"
21
+ # variation_strength 0.3
22
+ # size "1024x1024"
23
+ #
24
+ # description "Creates variations of logos"
25
+ # end
26
+ #
27
+ # result = LogoVariator.call(image: original_logo, count: 4)
28
+ #
29
+ class ImageVariator
30
+ extend DSL
31
+ include Execution
32
+
33
+ class << self
34
+ # Execute image variation with the given source image
35
+ #
36
+ # @param image [String, IO] Path, URL, or IO object of the source image
37
+ # @param options [Hash] Additional options (model, count, size, etc.)
38
+ # @return [ImageVariationResult] The result containing variation images
39
+ def call(image:, **options)
40
+ new(image: image, **options).call
41
+ end
42
+
43
+ # Ensure subclasses inherit DSL settings
44
+ def inherited(subclass)
45
+ super
46
+ subclass.instance_variable_set(:@model, @model)
47
+ subclass.instance_variable_set(:@size, @size)
48
+ subclass.instance_variable_set(:@variation_strength, @variation_strength)
49
+ subclass.instance_variable_set(:@version, @version)
50
+ subclass.instance_variable_set(:@description, @description)
51
+ subclass.instance_variable_set(:@cache_ttl, @cache_ttl)
52
+ end
53
+ end
54
+
55
+ attr_reader :image, :options, :tenant_id
56
+
57
+ # Initialize a new image variator instance
58
+ #
59
+ # @param image [String, IO] Source image (path, URL, or IO object)
60
+ # @param options [Hash] Additional options
61
+ # @option options [String] :model Model to use
62
+ # @option options [Integer] :count Number of variations to generate
63
+ # @option options [String] :size Output image size
64
+ # @option options [Float] :variation_strength How different variations should be (0.0-1.0)
65
+ # @option options [Object] :tenant Tenant for multi-tenancy
66
+ def initialize(image:, **options)
67
+ @image = image
68
+ @options = options
69
+ @tenant_id = nil
70
+ end
71
+
72
+ # Execute the image variation
73
+ #
74
+ # @return [ImageVariationResult] The result containing variation images
75
+ def call
76
+ execute
77
+ end
78
+ end
79
+ end
80
+ end
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "faraday"
3
+ require "net/http"
4
+ require "uri"
4
5
  require "json"
5
6
 
6
7
  module RubyLLM
@@ -177,34 +178,28 @@ module RubyLLM
177
178
  end
178
179
  end
179
180
 
180
- # Posts JSON to a URL using Faraday
181
+ # Posts JSON to a URL using Net::HTTP
181
182
  #
182
183
  # @param url [String] The URL
183
184
  # @param payload [Hash] The payload
184
- # @return [Faraday::Response]
185
+ # @return [Net::HTTPResponse]
185
186
  def post_json(url, payload)
186
- response = http_client.post(url) do |req|
187
- req.headers["Content-Type"] = "application/json"
188
- req.body = payload.to_json
187
+ uri = URI.parse(url)
188
+ http = Net::HTTP.new(uri.host, uri.port)
189
+ http.use_ssl = (uri.scheme == "https")
190
+ http.open_timeout = 5
191
+ http.read_timeout = 10
192
+
193
+ request = Net::HTTP::Post.new(uri.request_uri)
194
+ request["Content-Type"] = "application/json"
195
+ request.body = payload.to_json
196
+
197
+ response = http.request(request)
198
+ unless response.is_a?(Net::HTTPSuccess)
199
+ Rails.logger.warn("[RubyLLM::Agents::AlertManager] Webhook returned #{response.code}: #{response.body}")
189
200
  end
190
-
191
- unless response.success?
192
- Rails.logger.warn("[RubyLLM::Agents::AlertManager] Webhook returned #{response.status}: #{response.body}")
193
- end
194
-
195
201
  response
196
202
  end
197
-
198
- # Returns a configured Faraday HTTP client
199
- #
200
- # @return [Faraday::Connection]
201
- def http_client
202
- @http_client ||= Faraday.new do |conn|
203
- conn.options.open_timeout = 5
204
- conn.options.timeout = 10
205
- conn.adapter Faraday.default_adapter
206
- end
207
- end
208
203
  end
209
204
  end
210
205
  end
@@ -0,0 +1,145 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../cache_helper"
4
+
5
+ module RubyLLM
6
+ module Agents
7
+ module Budget
8
+ # Query methods for current spend, remaining budget, and status
9
+ #
10
+ # @api private
11
+ module BudgetQuery
12
+ extend CacheHelper
13
+
14
+ class << self
15
+ # Returns the current spend for a scope and period
16
+ #
17
+ # @param scope [Symbol] :global or :agent
18
+ # @param period [Symbol] :daily or :monthly
19
+ # @param agent_type [String, nil] Required when scope is :agent
20
+ # @param tenant_id [String, nil] The tenant identifier
21
+ # @return [Float] Current spend in USD
22
+ def current_spend(scope, period, agent_type: nil, tenant_id: nil)
23
+ key = SpendRecorder.budget_cache_key(scope, period, agent_type: agent_type, tenant_id: tenant_id)
24
+ (BudgetQuery.cache_read(key) || 0).to_f
25
+ end
26
+
27
+ # Returns the current token usage for a period (global only)
28
+ #
29
+ # @param period [Symbol] :daily or :monthly
30
+ # @param tenant_id [String, nil] The tenant identifier
31
+ # @return [Integer] Current token usage
32
+ def current_tokens(period, tenant_id: nil)
33
+ key = SpendRecorder.token_cache_key(period, tenant_id: tenant_id)
34
+ (BudgetQuery.cache_read(key) || 0).to_i
35
+ end
36
+
37
+ # Returns the remaining budget for a scope and period
38
+ #
39
+ # @param scope [Symbol] :global or :agent
40
+ # @param period [Symbol] :daily or :monthly
41
+ # @param agent_type [String, nil] Required when scope is :agent
42
+ # @param tenant_id [String, nil] The tenant identifier
43
+ # @param budget_config [Hash] Budget configuration
44
+ # @return [Float, nil] Remaining budget in USD, or nil if no limit configured
45
+ def remaining_budget(scope, period, agent_type: nil, tenant_id: nil, budget_config:)
46
+ limit = case [scope, period]
47
+ when [:global, :daily]
48
+ budget_config[:global_daily]
49
+ when [:global, :monthly]
50
+ budget_config[:global_monthly]
51
+ when [:agent, :daily]
52
+ budget_config[:per_agent_daily]&.dig(agent_type)
53
+ when [:agent, :monthly]
54
+ budget_config[:per_agent_monthly]&.dig(agent_type)
55
+ end
56
+
57
+ return nil unless limit
58
+
59
+ [limit - current_spend(scope, period, agent_type: agent_type, tenant_id: tenant_id), 0].max
60
+ end
61
+
62
+ # Returns the remaining token budget for a period (global only)
63
+ #
64
+ # @param period [Symbol] :daily or :monthly
65
+ # @param tenant_id [String, nil] The tenant identifier
66
+ # @param budget_config [Hash] Budget configuration
67
+ # @return [Integer, nil] Remaining token budget, or nil if no limit configured
68
+ def remaining_token_budget(period, tenant_id: nil, budget_config:)
69
+ limit = case period
70
+ when :daily
71
+ budget_config[:global_daily_tokens]
72
+ when :monthly
73
+ budget_config[:global_monthly_tokens]
74
+ end
75
+
76
+ return nil unless limit
77
+
78
+ [limit - current_tokens(period, tenant_id: tenant_id), 0].max
79
+ end
80
+
81
+ # Returns a summary of all budget statuses
82
+ #
83
+ # @param agent_type [String, nil] Optional agent type for per-agent budgets
84
+ # @param tenant_id [String, nil] The tenant identifier
85
+ # @param budget_config [Hash] Budget configuration
86
+ # @return [Hash] Budget status information
87
+ def status(agent_type: nil, tenant_id: nil, budget_config:)
88
+ {
89
+ tenant_id: tenant_id,
90
+ enabled: budget_config[:enabled],
91
+ enforcement: budget_config[:enforcement],
92
+ # Cost budgets
93
+ global_daily: budget_status(:global, :daily, budget_config[:global_daily], tenant_id: tenant_id),
94
+ global_monthly: budget_status(:global, :monthly, budget_config[:global_monthly], tenant_id: tenant_id),
95
+ per_agent_daily: agent_type ? budget_status(:agent, :daily, budget_config[:per_agent_daily]&.dig(agent_type), agent_type: agent_type, tenant_id: tenant_id) : nil,
96
+ per_agent_monthly: agent_type ? budget_status(:agent, :monthly, budget_config[:per_agent_monthly]&.dig(agent_type), agent_type: agent_type, tenant_id: tenant_id) : nil,
97
+ # Token budgets (global only)
98
+ global_daily_tokens: token_status(:daily, budget_config[:global_daily_tokens], tenant_id: tenant_id),
99
+ global_monthly_tokens: token_status(:monthly, budget_config[:global_monthly_tokens], tenant_id: tenant_id),
100
+ forecast: Forecaster.calculate_forecast(tenant_id: tenant_id, budget_config: budget_config)
101
+ }.compact
102
+ end
103
+
104
+ # Returns budget status for a scope/period
105
+ #
106
+ # @param scope [Symbol] :global or :agent
107
+ # @param period [Symbol] :daily or :monthly
108
+ # @param limit [Float, nil] The budget limit
109
+ # @param agent_type [String, nil] Required when scope is :agent
110
+ # @param tenant_id [String, nil] The tenant identifier
111
+ # @return [Hash, nil] Status hash or nil if no limit
112
+ def budget_status(scope, period, limit, agent_type: nil, tenant_id: nil)
113
+ return nil unless limit
114
+
115
+ current = current_spend(scope, period, agent_type: agent_type, tenant_id: tenant_id)
116
+ {
117
+ limit: limit,
118
+ current: current.round(6),
119
+ remaining: [limit - current, 0].max.round(6),
120
+ percentage_used: ((current / limit) * 100).round(2)
121
+ }
122
+ end
123
+
124
+ # Returns token status for a period
125
+ #
126
+ # @param period [Symbol] :daily or :monthly
127
+ # @param limit [Integer, nil] The token limit
128
+ # @param tenant_id [String, nil] The tenant identifier
129
+ # @return [Hash, nil] Status hash or nil if no limit
130
+ def token_status(period, limit, tenant_id: nil)
131
+ return nil unless limit
132
+
133
+ current = current_tokens(period, tenant_id: tenant_id)
134
+ {
135
+ limit: limit,
136
+ current: current,
137
+ remaining: [limit - current, 0].max,
138
+ percentage_used: ((current.to_f / limit) * 100).round(2)
139
+ }
140
+ end
141
+ end
142
+ end
143
+ end
144
+ end
145
+ end
@@ -0,0 +1,149 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module Agents
5
+ module Budget
6
+ # Resolves budget configuration for tenants and global settings
7
+ #
8
+ # Handles the resolution priority chain:
9
+ # 1. Runtime config passed to run()
10
+ # 2. tenant_config_resolver lambda
11
+ # 3. TenantBudget database record
12
+ # 4. Global configuration
13
+ #
14
+ # @api private
15
+ module ConfigResolver
16
+ class << self
17
+ # Resolves the current tenant ID
18
+ #
19
+ # @param explicit_tenant_id [String, nil] Explicitly passed tenant ID
20
+ # @return [String, nil] Resolved tenant ID or nil if multi-tenancy disabled
21
+ def resolve_tenant_id(explicit_tenant_id)
22
+ config = RubyLLM::Agents.configuration
23
+
24
+ # Ignore tenant_id entirely when multi-tenancy is disabled
25
+ return nil unless config.multi_tenancy_enabled?
26
+
27
+ # Use explicit tenant_id if provided, otherwise use resolver
28
+ return explicit_tenant_id if explicit_tenant_id.present?
29
+
30
+ config.tenant_resolver&.call
31
+ end
32
+
33
+ # Resolves budget configuration for a tenant
34
+ #
35
+ # Priority order:
36
+ # 1. runtime_config (passed to run())
37
+ # 2. tenant_config_resolver (configured lambda)
38
+ # 3. TenantBudget database record
39
+ # 4. Global configuration
40
+ #
41
+ # @param tenant_id [String, nil] The tenant identifier
42
+ # @param runtime_config [Hash, nil] Runtime config passed to run()
43
+ # @return [Hash] Budget configuration
44
+ def resolve_budget_config(tenant_id, runtime_config: nil)
45
+ config = RubyLLM::Agents.configuration
46
+
47
+ # Priority 1: Runtime config passed directly to run()
48
+ if runtime_config.present?
49
+ return normalize_budget_config(runtime_config, config)
50
+ end
51
+
52
+ # If multi-tenancy is disabled or no tenant, use global config
53
+ if tenant_id.nil? || !config.multi_tenancy_enabled?
54
+ return global_budget_config(config)
55
+ end
56
+
57
+ # Priority 2: tenant_config_resolver lambda
58
+ if config.tenant_config_resolver.present?
59
+ resolved_config = config.tenant_config_resolver.call(tenant_id)
60
+ if resolved_config.present?
61
+ return normalize_budget_config(resolved_config, config)
62
+ end
63
+ end
64
+
65
+ # Priority 3: Look up tenant-specific budget from database
66
+ tenant_budget = lookup_tenant_budget(tenant_id)
67
+
68
+ if tenant_budget
69
+ tenant_budget.to_budget_config
70
+ else
71
+ # Priority 4: Fall back to global config for unknown tenants
72
+ global_budget_config(config)
73
+ end
74
+ end
75
+
76
+ # Builds global budget config from configuration
77
+ #
78
+ # @param config [Configuration] The configuration object
79
+ # @return [Hash] Budget configuration
80
+ def global_budget_config(config)
81
+ {
82
+ enabled: config.budgets_enabled?,
83
+ enforcement: config.budget_enforcement,
84
+ global_daily: config.budgets&.dig(:global_daily),
85
+ global_monthly: config.budgets&.dig(:global_monthly),
86
+ per_agent_daily: config.budgets&.dig(:per_agent_daily),
87
+ per_agent_monthly: config.budgets&.dig(:per_agent_monthly),
88
+ global_daily_tokens: config.budgets&.dig(:global_daily_tokens),
89
+ global_monthly_tokens: config.budgets&.dig(:global_monthly_tokens)
90
+ }
91
+ end
92
+
93
+ # Normalizes runtime/resolver config to standard budget config format
94
+ #
95
+ # @param raw_config [Hash] Raw config from runtime or resolver
96
+ # @param global_config [Configuration] Global config for fallbacks
97
+ # @return [Hash] Normalized budget configuration
98
+ def normalize_budget_config(raw_config, global_config)
99
+ enforcement = raw_config[:enforcement]&.to_sym || global_config.budget_enforcement
100
+
101
+ {
102
+ enabled: enforcement != :none,
103
+ enforcement: enforcement,
104
+ # Cost/budget limits (USD)
105
+ global_daily: raw_config[:daily_budget_limit],
106
+ global_monthly: raw_config[:monthly_budget_limit],
107
+ per_agent_daily: raw_config[:per_agent_daily] || {},
108
+ per_agent_monthly: raw_config[:per_agent_monthly] || {},
109
+ # Token limits
110
+ global_daily_tokens: raw_config[:daily_token_limit],
111
+ global_monthly_tokens: raw_config[:monthly_token_limit]
112
+ }
113
+ end
114
+
115
+ # Safely looks up tenant budget, handling missing table
116
+ #
117
+ # @param tenant_id [String] The tenant identifier
118
+ # @return [TenantBudget, nil] The tenant budget or nil
119
+ def lookup_tenant_budget(tenant_id)
120
+ return nil unless tenant_budget_table_exists?
121
+
122
+ TenantBudget.for_tenant(tenant_id)
123
+ rescue StandardError => e
124
+ Rails.logger.warn("[RubyLLM::Agents] Failed to lookup tenant budget: #{e.message}")
125
+ nil
126
+ end
127
+
128
+ # Checks if the tenant_budgets table exists
129
+ #
130
+ # @return [Boolean] true if table exists
131
+ def tenant_budget_table_exists?
132
+ return @tenant_budget_table_exists if defined?(@tenant_budget_table_exists)
133
+
134
+ @tenant_budget_table_exists = ::ActiveRecord::Base.connection.table_exists?(:ruby_llm_agents_tenant_budgets)
135
+ rescue StandardError
136
+ @tenant_budget_table_exists = false
137
+ end
138
+
139
+ # Resets the memoized tenant budget table existence check (useful for testing)
140
+ #
141
+ # @return [void]
142
+ def reset_tenant_budget_table_check!
143
+ remove_instance_variable(:@tenant_budget_table_exists) if defined?(@tenant_budget_table_exists)
144
+ end
145
+ end
146
+ end
147
+ end
148
+ end
149
+ end