ruby_llm-agents 3.5.4 → 3.6.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 +4 -0
- data/app/controllers/ruby_llm/agents/dashboard_controller.rb +155 -10
- data/app/helpers/ruby_llm/agents/application_helper.rb +15 -1
- data/app/models/ruby_llm/agents/execution/replayable.rb +124 -0
- data/app/models/ruby_llm/agents/execution/scopes.rb +42 -1
- data/app/models/ruby_llm/agents/execution.rb +50 -1
- data/app/models/ruby_llm/agents/tenant/budgetable.rb +28 -4
- data/app/views/layouts/ruby_llm/agents/application.html.erb +41 -28
- data/app/views/ruby_llm/agents/agents/show.html.erb +16 -1
- data/app/views/ruby_llm/agents/dashboard/_top_tenants.html.erb +47 -0
- data/app/views/ruby_llm/agents/dashboard/index.html.erb +397 -100
- data/lib/generators/ruby_llm_agents/rename_agent_generator.rb +53 -0
- data/lib/generators/ruby_llm_agents/templates/rename_agent_migration.rb.tt +19 -0
- data/lib/ruby_llm/agents/agent_tool.rb +125 -0
- data/lib/ruby_llm/agents/audio/speaker.rb +5 -3
- data/lib/ruby_llm/agents/audio/speech_pricing.rb +63 -187
- data/lib/ruby_llm/agents/audio/transcriber.rb +5 -3
- data/lib/ruby_llm/agents/audio/transcription_pricing.rb +5 -7
- data/lib/ruby_llm/agents/base_agent.rb +144 -5
- data/lib/ruby_llm/agents/core/configuration.rb +178 -53
- data/lib/ruby_llm/agents/core/errors.rb +3 -77
- data/lib/ruby_llm/agents/core/instrumentation.rb +0 -17
- data/lib/ruby_llm/agents/core/version.rb +1 -1
- data/lib/ruby_llm/agents/dsl/base.rb +0 -8
- data/lib/ruby_llm/agents/dsl/queryable.rb +124 -0
- data/lib/ruby_llm/agents/dsl.rb +1 -0
- data/lib/ruby_llm/agents/image/concerns/image_operation_execution.rb +2 -1
- data/lib/ruby_llm/agents/image/generator/pricing.rb +75 -217
- data/lib/ruby_llm/agents/image/generator.rb +5 -3
- data/lib/ruby_llm/agents/infrastructure/attempt_tracker.rb +8 -0
- data/lib/ruby_llm/agents/infrastructure/circuit_breaker.rb +4 -2
- data/lib/ruby_llm/agents/pipeline/builder.rb +43 -0
- data/lib/ruby_llm/agents/pipeline/context.rb +11 -1
- data/lib/ruby_llm/agents/pipeline/executor.rb +1 -25
- data/lib/ruby_llm/agents/pipeline/middleware/budget.rb +26 -1
- data/lib/ruby_llm/agents/pipeline/middleware/cache.rb +18 -0
- data/lib/ruby_llm/agents/pipeline/middleware/instrumentation.rb +130 -3
- data/lib/ruby_llm/agents/pipeline/middleware/reliability.rb +29 -0
- data/lib/ruby_llm/agents/pipeline/middleware/tenant.rb +11 -4
- data/lib/ruby_llm/agents/pipeline.rb +0 -92
- data/lib/ruby_llm/agents/results/background_removal_result.rb +11 -1
- data/lib/ruby_llm/agents/results/base.rb +23 -1
- data/lib/ruby_llm/agents/results/embedding_result.rb +14 -1
- data/lib/ruby_llm/agents/results/image_analysis_result.rb +11 -1
- data/lib/ruby_llm/agents/results/image_edit_result.rb +11 -1
- data/lib/ruby_llm/agents/results/image_generation_result.rb +12 -3
- data/lib/ruby_llm/agents/results/image_pipeline_result.rb +11 -1
- data/lib/ruby_llm/agents/results/image_transform_result.rb +11 -1
- data/lib/ruby_llm/agents/results/image_upscale_result.rb +11 -1
- data/lib/ruby_llm/agents/results/image_variation_result.rb +11 -1
- data/lib/ruby_llm/agents/results/speech_result.rb +20 -1
- data/lib/ruby_llm/agents/results/transcription_result.rb +20 -1
- data/lib/ruby_llm/agents/text/embedder.rb +23 -18
- data/lib/ruby_llm/agents.rb +70 -5
- data/lib/tasks/ruby_llm_agents.rake +21 -0
- metadata +7 -6
- data/lib/ruby_llm/agents/infrastructure/reliability/breaker_manager.rb +0 -80
- data/lib/ruby_llm/agents/infrastructure/reliability/execution_constraints.rb +0 -69
- data/lib/ruby_llm/agents/infrastructure/reliability/executor.rb +0 -125
- data/lib/ruby_llm/agents/infrastructure/reliability/fallback_routing.rb +0 -72
- data/lib/ruby_llm/agents/infrastructure/reliability/retry_strategy.rb +0 -82
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
module Agents
|
|
5
|
+
module DSL
|
|
6
|
+
# Adds execution querying capabilities to agent classes.
|
|
7
|
+
#
|
|
8
|
+
# Mixed into BaseAgent via `extend DSL::Queryable`, making all methods
|
|
9
|
+
# available as class methods on agent classes.
|
|
10
|
+
#
|
|
11
|
+
# @example Basic queries
|
|
12
|
+
# SupportAgent.executions.successful.recent
|
|
13
|
+
# SupportAgent.executions.today.expensive(0.50)
|
|
14
|
+
#
|
|
15
|
+
# @example Convenience methods
|
|
16
|
+
# SupportAgent.last_run
|
|
17
|
+
# SupportAgent.stats
|
|
18
|
+
# SupportAgent.total_spent(since: 1.week)
|
|
19
|
+
#
|
|
20
|
+
module Queryable
|
|
21
|
+
# Returns an ActiveRecord::Relation scoped to this agent's executions.
|
|
22
|
+
#
|
|
23
|
+
# @return [ActiveRecord::Relation]
|
|
24
|
+
#
|
|
25
|
+
# @example
|
|
26
|
+
# SupportAgent.executions.successful.last(5)
|
|
27
|
+
# SupportAgent.executions.where("total_cost > ?", 0.01)
|
|
28
|
+
#
|
|
29
|
+
def executions
|
|
30
|
+
RubyLLM::Agents::Execution.by_agent(name)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Returns the most recent execution for this agent.
|
|
34
|
+
#
|
|
35
|
+
# @return [RubyLLM::Agents::Execution, nil]
|
|
36
|
+
#
|
|
37
|
+
def last_run
|
|
38
|
+
executions.order(created_at: :desc).first
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Returns recent failed executions.
|
|
42
|
+
#
|
|
43
|
+
# @param since [ActiveSupport::Duration] Time window (default: 24.hours)
|
|
44
|
+
# @return [ActiveRecord::Relation]
|
|
45
|
+
#
|
|
46
|
+
def failures(since: 24.hours)
|
|
47
|
+
executions.failed.where("created_at > ?", since.ago)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Returns total cost spent by this agent.
|
|
51
|
+
#
|
|
52
|
+
# @param since [ActiveSupport::Duration, nil] Optional time window
|
|
53
|
+
# @return [BigDecimal] Total cost in USD
|
|
54
|
+
#
|
|
55
|
+
def total_spent(since: nil)
|
|
56
|
+
scope = executions
|
|
57
|
+
scope = scope.where("created_at > ?", since.ago) if since
|
|
58
|
+
scope.sum(:total_cost)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Returns a stats summary hash for this agent.
|
|
62
|
+
#
|
|
63
|
+
# @param since [ActiveSupport::Duration, nil] Time window
|
|
64
|
+
# @return [Hash] Stats summary
|
|
65
|
+
#
|
|
66
|
+
def stats(since: nil)
|
|
67
|
+
scope = executions
|
|
68
|
+
scope = scope.where("created_at > ?", since.ago) if since
|
|
69
|
+
|
|
70
|
+
total = scope.count
|
|
71
|
+
successful = scope.successful.count
|
|
72
|
+
|
|
73
|
+
{
|
|
74
|
+
total: total,
|
|
75
|
+
successful: successful,
|
|
76
|
+
failed: scope.failed.count,
|
|
77
|
+
success_rate: total.zero? ? 0.0 : (successful.to_f / total * 100).round(1),
|
|
78
|
+
avg_duration_ms: scope.average(:duration_ms)&.round,
|
|
79
|
+
avg_cost: total.zero? ? 0 : (scope.sum(:total_cost).to_f / total).round(6),
|
|
80
|
+
total_cost: scope.sum(:total_cost),
|
|
81
|
+
total_tokens: scope.sum(:total_tokens),
|
|
82
|
+
avg_tokens: scope.average(:total_tokens)&.round
|
|
83
|
+
}
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Returns cost breakdown by model for this agent.
|
|
87
|
+
#
|
|
88
|
+
# @param since [ActiveSupport::Duration, nil] Time window
|
|
89
|
+
# @return [Hash{String => Hash}] Costs per model
|
|
90
|
+
#
|
|
91
|
+
def cost_by_model(since: nil)
|
|
92
|
+
scope = executions
|
|
93
|
+
scope = scope.where("created_at > ?", since.ago) if since
|
|
94
|
+
|
|
95
|
+
scope.group(:model_id).pluck(
|
|
96
|
+
:model_id,
|
|
97
|
+
Arel.sql("COUNT(*)"),
|
|
98
|
+
Arel.sql("SUM(total_cost)"),
|
|
99
|
+
Arel.sql("AVG(total_cost)")
|
|
100
|
+
).each_with_object({}) do |(model, count, total, avg), hash|
|
|
101
|
+
hash[model] = {
|
|
102
|
+
count: count,
|
|
103
|
+
total_cost: total&.to_f&.round(6) || 0,
|
|
104
|
+
avg_cost: avg&.to_f&.round(6) || 0
|
|
105
|
+
}
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# Returns executions matching specific parameter values.
|
|
110
|
+
#
|
|
111
|
+
# @param params [Hash] Parameter key-value pairs to match
|
|
112
|
+
# @return [ActiveRecord::Relation]
|
|
113
|
+
#
|
|
114
|
+
def with_params(**params)
|
|
115
|
+
scope = executions
|
|
116
|
+
params.each do |key, value|
|
|
117
|
+
scope = scope.with_parameter(key, value)
|
|
118
|
+
end
|
|
119
|
+
scope
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
end
|
data/lib/ruby_llm/agents/dsl.rb
CHANGED
|
@@ -85,7 +85,8 @@ module RubyLLM
|
|
|
85
85
|
if config.async_logging && defined?(ExecutionLoggerJob)
|
|
86
86
|
ExecutionLoggerJob.perform_later(execution_data)
|
|
87
87
|
else
|
|
88
|
-
RubyLLM::Agents::Execution.create!(execution_data)
|
|
88
|
+
execution = RubyLLM::Agents::Execution.create!(execution_data)
|
|
89
|
+
result.execution_id = execution.id if result.respond_to?(:execution_id=)
|
|
89
90
|
end
|
|
90
91
|
rescue => e
|
|
91
92
|
Rails.logger.error("[RubyLLM::Agents] Failed to record #{execution_type} execution: #{e.message}") if defined?(Rails)
|
|
@@ -1,17 +1,20 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
require_relative "../../pricing/data_store"
|
|
4
|
+
require_relative "../../pricing/ruby_llm_adapter"
|
|
5
|
+
require_relative "../../pricing/litellm_adapter"
|
|
5
6
|
|
|
6
7
|
module RubyLLM
|
|
7
8
|
module Agents
|
|
8
9
|
class ImageGenerator
|
|
9
|
-
# Dynamic pricing resolution for image generation models
|
|
10
|
+
# Dynamic pricing resolution for image generation models.
|
|
10
11
|
#
|
|
11
|
-
# Uses a three-tier strategy:
|
|
12
|
-
# 1.
|
|
13
|
-
# 2.
|
|
14
|
-
# 3.
|
|
12
|
+
# Uses a three-tier strategy (no hardcoded prices):
|
|
13
|
+
# 1. Configurable pricing table - user overrides
|
|
14
|
+
# 2. RubyLLM gem (local, no HTTP) - model registry pricing
|
|
15
|
+
# 3. LiteLLM (via shared DataStore) - comprehensive, community-maintained
|
|
16
|
+
#
|
|
17
|
+
# When no pricing is found, returns 0 to signal unknown cost.
|
|
15
18
|
#
|
|
16
19
|
# @example Get price for a model
|
|
17
20
|
# Pricing.cost_per_image("gpt-image-1", size: "1024x1024", quality: "hd")
|
|
@@ -24,9 +27,6 @@ module RubyLLM
|
|
|
24
27
|
module Pricing
|
|
25
28
|
extend self
|
|
26
29
|
|
|
27
|
-
LITELLM_PRICING_URL = "https://raw.githubusercontent.com/BerriAI/litellm/main/model_prices_and_context_window.json"
|
|
28
|
-
DEFAULT_CACHE_TTL = 24 * 60 * 60 # 24 hours in seconds
|
|
29
|
-
|
|
30
30
|
# Calculate total cost for image generation
|
|
31
31
|
#
|
|
32
32
|
# @param model_id [String] The model identifier
|
|
@@ -44,170 +44,50 @@ module RubyLLM
|
|
|
44
44
|
# @param model_id [String] The model identifier
|
|
45
45
|
# @param size [String] Image size
|
|
46
46
|
# @param quality [String] Quality setting
|
|
47
|
-
# @return [Float] Cost per image in USD
|
|
47
|
+
# @return [Float] Cost per image in USD (0 if unknown)
|
|
48
48
|
def cost_per_image(model_id, size: nil, quality: nil)
|
|
49
|
-
# 1
|
|
50
|
-
if (litellm_price = from_litellm(model_id, size, quality))
|
|
51
|
-
return litellm_price
|
|
52
|
-
end
|
|
53
|
-
|
|
54
|
-
# 2. Try configurable pricing table
|
|
49
|
+
# Tier 1: User-configurable pricing table
|
|
55
50
|
if (config_price = from_config(model_id, size, quality))
|
|
56
51
|
return config_price
|
|
57
52
|
end
|
|
58
53
|
|
|
59
|
-
#
|
|
60
|
-
|
|
54
|
+
# Tier 2: RubyLLM gem (local, no HTTP)
|
|
55
|
+
if (ruby_llm_price = from_ruby_llm(model_id))
|
|
56
|
+
return ruby_llm_price
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Tier 3: LiteLLM (via shared DataStore + adapter)
|
|
60
|
+
if (litellm_price = from_litellm(model_id, size, quality))
|
|
61
|
+
return litellm_price
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# No pricing found — return user-configured default or 0
|
|
65
|
+
config.default_image_cost || 0
|
|
61
66
|
end
|
|
62
67
|
|
|
63
|
-
# Refresh pricing data
|
|
68
|
+
# Refresh pricing data
|
|
64
69
|
#
|
|
65
|
-
# @return [
|
|
70
|
+
# @return [void]
|
|
66
71
|
def refresh!
|
|
67
|
-
|
|
68
|
-
@litellm_fetched_at = nil
|
|
69
|
-
litellm_data
|
|
72
|
+
Agents::Pricing::DataStore.refresh!
|
|
70
73
|
end
|
|
71
74
|
|
|
72
|
-
#
|
|
75
|
+
# Expose all known pricing for debugging/console inspection
|
|
73
76
|
#
|
|
74
77
|
# @return [Hash] Merged pricing from all sources
|
|
75
78
|
def all_pricing
|
|
76
79
|
{
|
|
77
80
|
litellm: litellm_image_models,
|
|
78
|
-
configured: config.image_model_pricing || {}
|
|
79
|
-
fallbacks: fallback_pricing_table
|
|
81
|
+
configured: config.image_model_pricing || {}
|
|
80
82
|
}
|
|
81
83
|
end
|
|
82
84
|
|
|
83
85
|
private
|
|
84
86
|
|
|
85
|
-
#
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
return nil unless data
|
|
89
|
-
|
|
90
|
-
# Try exact match first
|
|
91
|
-
model_data = find_litellm_model(data, model_id, size, quality)
|
|
92
|
-
return nil unless model_data
|
|
93
|
-
|
|
94
|
-
extract_litellm_price(model_data, size, quality)
|
|
95
|
-
end
|
|
96
|
-
|
|
97
|
-
def find_litellm_model(data, model_id, size, quality)
|
|
98
|
-
normalized = normalize_model_id(model_id)
|
|
99
|
-
|
|
100
|
-
# Try various key formats LiteLLM uses
|
|
101
|
-
candidates = [
|
|
102
|
-
model_id,
|
|
103
|
-
normalized,
|
|
104
|
-
"#{size}/#{model_id}",
|
|
105
|
-
"#{size}/#{quality}/#{model_id}",
|
|
106
|
-
"aiml/#{normalized}",
|
|
107
|
-
"together_ai/#{normalized}"
|
|
108
|
-
]
|
|
109
|
-
|
|
110
|
-
candidates.each do |key|
|
|
111
|
-
return data[key] if data[key]
|
|
112
|
-
end
|
|
113
|
-
|
|
114
|
-
# Fuzzy match by model name pattern
|
|
115
|
-
data.find do |key, _value|
|
|
116
|
-
key_lower = key.to_s.downcase
|
|
117
|
-
normalized_lower = normalized.downcase
|
|
118
|
-
|
|
119
|
-
key_lower.include?(normalized_lower) ||
|
|
120
|
-
normalized_lower.include?(key_lower.split("/").last.to_s)
|
|
121
|
-
end&.last
|
|
122
|
-
end
|
|
123
|
-
|
|
124
|
-
def extract_litellm_price(model_data, size, quality)
|
|
125
|
-
# LiteLLM uses different pricing fields for images
|
|
126
|
-
if model_data["input_cost_per_image"]
|
|
127
|
-
return model_data["input_cost_per_image"]
|
|
128
|
-
end
|
|
129
|
-
|
|
130
|
-
if model_data["input_cost_per_pixel"] && size
|
|
131
|
-
width, height = size.split("x").map(&:to_i)
|
|
132
|
-
pixels = width * height
|
|
133
|
-
return (model_data["input_cost_per_pixel"] * pixels).round(6)
|
|
134
|
-
end
|
|
135
|
-
|
|
136
|
-
# Some models have quality-based pricing
|
|
137
|
-
if quality == "hd" && model_data["input_cost_per_image_hd"]
|
|
138
|
-
return model_data["input_cost_per_image_hd"]
|
|
139
|
-
end
|
|
140
|
-
|
|
141
|
-
nil
|
|
142
|
-
end
|
|
143
|
-
|
|
144
|
-
def litellm_data
|
|
145
|
-
return @litellm_data if @litellm_data && !cache_expired?
|
|
146
|
-
|
|
147
|
-
@litellm_data = fetch_litellm_data
|
|
148
|
-
@litellm_fetched_at = Time.now
|
|
149
|
-
@litellm_data
|
|
150
|
-
end
|
|
151
|
-
|
|
152
|
-
def fetch_litellm_data
|
|
153
|
-
# Use Rails cache if available
|
|
154
|
-
if defined?(Rails) && Rails.cache
|
|
155
|
-
Rails.cache.fetch("litellm_pricing_data", expires_in: cache_ttl) do
|
|
156
|
-
fetch_from_url
|
|
157
|
-
end
|
|
158
|
-
else
|
|
159
|
-
fetch_from_url
|
|
160
|
-
end
|
|
161
|
-
rescue => e
|
|
162
|
-
warn "[RubyLLM::Agents] Failed to fetch LiteLLM pricing: #{e.message}"
|
|
163
|
-
{}
|
|
164
|
-
end
|
|
165
|
-
|
|
166
|
-
def fetch_from_url
|
|
167
|
-
uri = URI(config.litellm_pricing_url || LITELLM_PRICING_URL)
|
|
168
|
-
http = Net::HTTP.new(uri.host, uri.port)
|
|
169
|
-
http.use_ssl = uri.scheme == "https"
|
|
170
|
-
http.open_timeout = 5
|
|
171
|
-
http.read_timeout = 10
|
|
172
|
-
|
|
173
|
-
request = Net::HTTP::Get.new(uri)
|
|
174
|
-
response = http.request(request)
|
|
87
|
+
# ============================================================
|
|
88
|
+
# Tier 1: User-configurable pricing table
|
|
89
|
+
# ============================================================
|
|
175
90
|
|
|
176
|
-
if response.is_a?(Net::HTTPSuccess)
|
|
177
|
-
JSON.parse(response.body)
|
|
178
|
-
else
|
|
179
|
-
{}
|
|
180
|
-
end
|
|
181
|
-
rescue => e
|
|
182
|
-
warn "[RubyLLM::Agents] HTTP error fetching LiteLLM pricing: #{e.message}"
|
|
183
|
-
{}
|
|
184
|
-
end
|
|
185
|
-
|
|
186
|
-
def cache_expired?
|
|
187
|
-
return true unless @litellm_fetched_at
|
|
188
|
-
Time.now - @litellm_fetched_at > cache_ttl
|
|
189
|
-
end
|
|
190
|
-
|
|
191
|
-
def cache_ttl
|
|
192
|
-
ttl = config.litellm_pricing_cache_ttl
|
|
193
|
-
return DEFAULT_CACHE_TTL unless ttl
|
|
194
|
-
|
|
195
|
-
# Handle ActiveSupport::Duration
|
|
196
|
-
ttl.respond_to?(:to_i) ? ttl.to_i : ttl
|
|
197
|
-
end
|
|
198
|
-
|
|
199
|
-
# Get image-specific models from LiteLLM data
|
|
200
|
-
def litellm_image_models
|
|
201
|
-
litellm_data.select do |key, value|
|
|
202
|
-
value.is_a?(Hash) && (
|
|
203
|
-
value["input_cost_per_image"] ||
|
|
204
|
-
value["input_cost_per_pixel"] ||
|
|
205
|
-
key.to_s.match?(/dall-e|flux|sdxl|stable|imagen|image/i)
|
|
206
|
-
)
|
|
207
|
-
end
|
|
208
|
-
end
|
|
209
|
-
|
|
210
|
-
# Fetch from configurable pricing table
|
|
211
91
|
def from_config(model_id, size, quality)
|
|
212
92
|
table = config.image_model_pricing
|
|
213
93
|
return nil unless table
|
|
@@ -251,80 +131,58 @@ module RubyLLM
|
|
|
251
131
|
pricing[:base] || pricing["base"] || pricing[:default] || pricing["default"] || pricing[:standard] || pricing["standard"]
|
|
252
132
|
end
|
|
253
133
|
|
|
254
|
-
#
|
|
255
|
-
|
|
256
|
-
|
|
134
|
+
# ============================================================
|
|
135
|
+
# Tier 2: RubyLLM gem (local, no HTTP)
|
|
136
|
+
# ============================================================
|
|
257
137
|
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
when /imagen/i
|
|
264
|
-
0.02
|
|
265
|
-
when /flux.*pro.*ultra/i
|
|
266
|
-
0.063
|
|
267
|
-
when /flux.*pro/i
|
|
268
|
-
0.05
|
|
269
|
-
when /flux.*dev/i
|
|
270
|
-
0.025
|
|
271
|
-
when /flux.*schnell/i
|
|
272
|
-
0.003
|
|
273
|
-
when /sdxl.*lightning/i
|
|
274
|
-
0.002
|
|
275
|
-
when /sdxl|stable-diffusion-xl/i
|
|
276
|
-
0.04
|
|
277
|
-
when /stable-diffusion/i
|
|
278
|
-
0.02
|
|
279
|
-
when /ideogram/i
|
|
280
|
-
0.04
|
|
281
|
-
when /recraft/i
|
|
282
|
-
0.04
|
|
283
|
-
when /real-esrgan|upscal/i
|
|
284
|
-
0.01
|
|
285
|
-
when /blip|caption|analyz/i
|
|
286
|
-
0.001
|
|
287
|
-
when /segment|background|rembg/i
|
|
288
|
-
0.01
|
|
289
|
-
else
|
|
290
|
-
config.default_image_cost || 0.04
|
|
291
|
-
end
|
|
138
|
+
def from_ruby_llm(model_id)
|
|
139
|
+
data = Agents::Pricing::RubyLLMAdapter.find_model(model_id)
|
|
140
|
+
return nil unless data
|
|
141
|
+
|
|
142
|
+
data[:input_cost_per_image]
|
|
292
143
|
end
|
|
293
144
|
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
145
|
+
# ============================================================
|
|
146
|
+
# Tier 3: LiteLLM (via shared DataStore + adapter)
|
|
147
|
+
# ============================================================
|
|
297
148
|
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
end
|
|
149
|
+
def from_litellm(model_id, size, quality)
|
|
150
|
+
data = Agents::Pricing::LiteLLMAdapter.find_model(model_id)
|
|
151
|
+
return nil unless data
|
|
152
|
+
|
|
153
|
+
extract_image_price(data, size, quality)
|
|
304
154
|
end
|
|
305
155
|
|
|
306
|
-
def
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
when "256x256" then 0.016
|
|
311
|
-
else 0.02
|
|
156
|
+
def extract_image_price(data, size, quality)
|
|
157
|
+
# Check quality-specific pricing first when HD requested
|
|
158
|
+
if quality == "hd" && data[:input_cost_per_image_hd]
|
|
159
|
+
return data[:input_cost_per_image_hd]
|
|
312
160
|
end
|
|
161
|
+
|
|
162
|
+
if data[:input_cost_per_image]
|
|
163
|
+
return data[:input_cost_per_image]
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
if data[:input_cost_per_pixel] && size
|
|
167
|
+
width, height = size.split("x").map(&:to_i)
|
|
168
|
+
pixels = width * height
|
|
169
|
+
return (data[:input_cost_per_pixel] * pixels).round(6)
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
nil
|
|
313
173
|
end
|
|
314
174
|
|
|
315
|
-
def
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
"ideogram-2" => 0.04
|
|
327
|
-
}
|
|
175
|
+
def litellm_image_models
|
|
176
|
+
data = Agents::Pricing::DataStore.litellm_data
|
|
177
|
+
return {} unless data.is_a?(Hash)
|
|
178
|
+
|
|
179
|
+
data.select do |key, value|
|
|
180
|
+
value.is_a?(Hash) && (
|
|
181
|
+
value["input_cost_per_image"] ||
|
|
182
|
+
value["input_cost_per_pixel"] ||
|
|
183
|
+
key.to_s.match?(/dall-e|flux|sdxl|stable|imagen|image/i)
|
|
184
|
+
)
|
|
185
|
+
end
|
|
328
186
|
end
|
|
329
187
|
|
|
330
188
|
def parse_pixels(size)
|
|
@@ -248,7 +248,8 @@ module RubyLLM
|
|
|
248
248
|
images: images,
|
|
249
249
|
started_at: context.started_at || execution_started_at,
|
|
250
250
|
completed_at: execution_completed_at,
|
|
251
|
-
tenant_id: context.tenant_id
|
|
251
|
+
tenant_id: context.tenant_id,
|
|
252
|
+
execution_id: context.execution_id
|
|
252
253
|
)
|
|
253
254
|
|
|
254
255
|
# Update context with cost info
|
|
@@ -348,7 +349,7 @@ module RubyLLM
|
|
|
348
349
|
end
|
|
349
350
|
|
|
350
351
|
# Build successful result
|
|
351
|
-
def build_result(images:, started_at:, completed_at:, tenant_id:)
|
|
352
|
+
def build_result(images:, started_at:, completed_at:, tenant_id:, execution_id: nil)
|
|
352
353
|
ImageGenerationResult.new(
|
|
353
354
|
images: images,
|
|
354
355
|
prompt: prompt,
|
|
@@ -359,7 +360,8 @@ module RubyLLM
|
|
|
359
360
|
started_at: started_at,
|
|
360
361
|
completed_at: completed_at,
|
|
361
362
|
tenant_id: tenant_id,
|
|
362
|
-
generator_class: self.class.name
|
|
363
|
+
generator_class: self.class.name,
|
|
364
|
+
execution_id: execution_id
|
|
363
365
|
)
|
|
364
366
|
end
|
|
365
367
|
|
|
@@ -121,6 +121,8 @@ module RubyLLM
|
|
|
121
121
|
|
|
122
122
|
# Finds the last failed attempt
|
|
123
123
|
#
|
|
124
|
+
# Useful for debugging in rails console.
|
|
125
|
+
#
|
|
124
126
|
# @return [Hash, nil] The last failed attempt or nil
|
|
125
127
|
def last_failed_attempt
|
|
126
128
|
@attempts.reverse.find { |a| a[:error_class].present? }
|
|
@@ -189,6 +191,8 @@ module RubyLLM
|
|
|
189
191
|
|
|
190
192
|
# Calculates total duration across all attempts
|
|
191
193
|
#
|
|
194
|
+
# Useful for debugging in rails console.
|
|
195
|
+
#
|
|
192
196
|
# @return [Integer] Total duration in milliseconds
|
|
193
197
|
def total_duration_ms
|
|
194
198
|
@attempts.sum { |a| a[:duration_ms] || 0 }
|
|
@@ -203,6 +207,8 @@ module RubyLLM
|
|
|
203
207
|
|
|
204
208
|
# Returns the number of failed attempts
|
|
205
209
|
#
|
|
210
|
+
# Useful for debugging in rails console.
|
|
211
|
+
#
|
|
206
212
|
# @return [Integer] Number of failed attempts
|
|
207
213
|
def failed_attempts_count
|
|
208
214
|
@attempts.count { |a| a[:error_class].present? }
|
|
@@ -210,6 +216,8 @@ module RubyLLM
|
|
|
210
216
|
|
|
211
217
|
# Returns the number of short-circuited attempts
|
|
212
218
|
#
|
|
219
|
+
# Useful for debugging in rails console.
|
|
220
|
+
#
|
|
213
221
|
# @return [Integer] Number of short-circuited attempts
|
|
214
222
|
def short_circuited_count
|
|
215
223
|
@attempts.count { |a| a[:short_circuited] }
|
|
@@ -123,17 +123,19 @@ module RubyLLM
|
|
|
123
123
|
|
|
124
124
|
# Returns the time remaining until the breaker closes
|
|
125
125
|
#
|
|
126
|
+
# Useful for debugging circuit breaker state in rails console.
|
|
127
|
+
#
|
|
126
128
|
# @return [Integer, nil] Seconds until cooldown expires, or nil if not open
|
|
127
129
|
def time_until_close
|
|
128
130
|
return nil unless open?
|
|
129
131
|
|
|
130
|
-
# We can't easily get TTL from Rails.cache, so this is an approximation
|
|
131
|
-
# In a real implementation, you might store the open time as well
|
|
132
132
|
cooldown_seconds
|
|
133
133
|
end
|
|
134
134
|
|
|
135
135
|
# Returns status information for the circuit breaker
|
|
136
136
|
#
|
|
137
|
+
# Useful for debugging circuit breaker state in rails console.
|
|
138
|
+
#
|
|
137
139
|
# @return [Hash] Status information including open state and failure count
|
|
138
140
|
def status
|
|
139
141
|
{
|
|
@@ -144,6 +144,12 @@ module RubyLLM
|
|
|
144
144
|
|
|
145
145
|
# Reliability (if agent has retries or fallbacks configured)
|
|
146
146
|
builder.use(Middleware::Reliability) if reliability_enabled?(agent_class)
|
|
147
|
+
|
|
148
|
+
# Global custom middleware
|
|
149
|
+
apply_custom_middleware!(builder, global_middleware_entries)
|
|
150
|
+
|
|
151
|
+
# Per-agent custom middleware
|
|
152
|
+
apply_custom_middleware!(builder, agent_middleware_entries(agent_class))
|
|
147
153
|
end
|
|
148
154
|
end
|
|
149
155
|
|
|
@@ -159,6 +165,43 @@ module RubyLLM
|
|
|
159
165
|
|
|
160
166
|
private
|
|
161
167
|
|
|
168
|
+
# Returns global custom middleware entries from configuration
|
|
169
|
+
#
|
|
170
|
+
# @return [Array<Hash>] Middleware entries
|
|
171
|
+
def global_middleware_entries
|
|
172
|
+
RubyLLM::Agents.configuration.middleware_stack
|
|
173
|
+
rescue
|
|
174
|
+
[]
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
# Returns per-agent custom middleware entries
|
|
178
|
+
#
|
|
179
|
+
# @param agent_class [Class] The agent class
|
|
180
|
+
# @return [Array<Hash>] Middleware entries
|
|
181
|
+
def agent_middleware_entries(agent_class)
|
|
182
|
+
return [] unless agent_class&.respond_to?(:agent_middleware)
|
|
183
|
+
|
|
184
|
+
agent_class.agent_middleware
|
|
185
|
+
rescue
|
|
186
|
+
[]
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
# Applies custom middleware entries to a builder
|
|
190
|
+
#
|
|
191
|
+
# @param builder [Builder] The builder to modify
|
|
192
|
+
# @param entries [Array<Hash>] Middleware entries with :klass, :before, :after
|
|
193
|
+
def apply_custom_middleware!(builder, entries)
|
|
194
|
+
entries.each do |entry|
|
|
195
|
+
if entry[:before]
|
|
196
|
+
builder.insert_before(entry[:before], entry[:klass])
|
|
197
|
+
elsif entry[:after]
|
|
198
|
+
builder.insert_after(entry[:after], entry[:klass])
|
|
199
|
+
else
|
|
200
|
+
builder.use(entry[:klass])
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
end
|
|
204
|
+
|
|
162
205
|
# Check if budgets are enabled globally
|
|
163
206
|
#
|
|
164
207
|
# @return [Boolean]
|
|
@@ -34,6 +34,9 @@ module RubyLLM
|
|
|
34
34
|
# Execution tracking (set by Instrumentation middleware)
|
|
35
35
|
attr_accessor :started_at, :completed_at, :attempt, :attempts_made, :execution_id
|
|
36
36
|
|
|
37
|
+
# Execution hierarchy (agent-as-tool)
|
|
38
|
+
attr_accessor :parent_execution_id, :root_execution_id
|
|
39
|
+
|
|
37
40
|
# Result data (set by core execute method)
|
|
38
41
|
attr_accessor :output, :error, :cached
|
|
39
42
|
|
|
@@ -59,13 +62,17 @@ module RubyLLM
|
|
|
59
62
|
# @param skip_cache [Boolean] Whether to skip caching
|
|
60
63
|
# @param stream_block [Proc, nil] Block for streaming
|
|
61
64
|
# @param options [Hash] Additional options passed to the agent
|
|
62
|
-
def initialize(input:, agent_class:, agent_instance: nil, model: nil, tenant: nil, skip_cache: false, stream_block: nil, **options)
|
|
65
|
+
def initialize(input:, agent_class:, agent_instance: nil, model: nil, tenant: nil, skip_cache: false, stream_block: nil, parent_execution_id: nil, root_execution_id: nil, **options)
|
|
63
66
|
@input = input
|
|
64
67
|
@agent_class = agent_class
|
|
65
68
|
@agent_instance = agent_instance
|
|
66
69
|
@agent_type = extract_agent_type(agent_class)
|
|
67
70
|
@model = model || extract_model(agent_class)
|
|
68
71
|
|
|
72
|
+
# Execution hierarchy
|
|
73
|
+
@parent_execution_id = parent_execution_id
|
|
74
|
+
@root_execution_id = root_execution_id
|
|
75
|
+
|
|
69
76
|
# Store tenant in options for middleware to resolve
|
|
70
77
|
@options = options.merge(tenant: tenant).compact
|
|
71
78
|
|
|
@@ -202,6 +209,9 @@ module RubyLLM
|
|
|
202
209
|
new_ctx.tenant_config = @tenant_config
|
|
203
210
|
new_ctx.started_at = @started_at
|
|
204
211
|
new_ctx.attempts_made = @attempts_made
|
|
212
|
+
# Preserve execution hierarchy
|
|
213
|
+
new_ctx.parent_execution_id = @parent_execution_id
|
|
214
|
+
new_ctx.root_execution_id = @root_execution_id
|
|
205
215
|
new_ctx
|
|
206
216
|
end
|
|
207
217
|
|