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,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 "
|
|
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
|
|
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 [
|
|
185
|
+
# @return [Net::HTTPResponse]
|
|
185
186
|
def post_json(url, payload)
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
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
|