ruby_llm-agents 3.7.2 → 3.9.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 +30 -10
- data/app/controllers/ruby_llm/agents/agents_controller.rb +14 -141
- data/app/controllers/ruby_llm/agents/dashboard_controller.rb +12 -166
- data/app/controllers/ruby_llm/agents/executions_controller.rb +1 -1
- data/app/controllers/ruby_llm/agents/requests_controller.rb +117 -0
- data/app/helpers/ruby_llm/agents/application_helper.rb +38 -0
- data/app/models/ruby_llm/agents/execution/analytics.rb +302 -103
- data/app/models/ruby_llm/agents/execution.rb +76 -54
- data/app/models/ruby_llm/agents/execution_detail.rb +2 -0
- data/app/models/ruby_llm/agents/tenant.rb +39 -0
- data/app/services/ruby_llm/agents/agent_registry.rb +98 -0
- data/app/views/layouts/ruby_llm/agents/application.html.erb +4 -2
- data/app/views/ruby_llm/agents/executions/_list.html.erb +3 -17
- data/app/views/ruby_llm/agents/requests/index.html.erb +153 -0
- data/app/views/ruby_llm/agents/requests/show.html.erb +136 -0
- data/config/routes.rb +2 -0
- data/lib/generators/ruby_llm_agents/agent_generator.rb +2 -2
- data/lib/generators/ruby_llm_agents/demo_generator.rb +102 -0
- data/lib/generators/ruby_llm_agents/doctor_generator.rb +196 -0
- data/lib/generators/ruby_llm_agents/install_generator.rb +7 -19
- data/lib/generators/ruby_llm_agents/templates/add_dashboard_performance_indexes_migration.rb.tt +23 -0
- data/lib/generators/ruby_llm_agents/templates/agent.rb.tt +27 -80
- data/lib/generators/ruby_llm_agents/templates/application_agent.rb.tt +18 -51
- data/lib/generators/ruby_llm_agents/templates/initializer.rb.tt +19 -17
- data/lib/generators/ruby_llm_agents/templates/migration.rb.tt +3 -0
- data/lib/generators/ruby_llm_agents/upgrade_generator.rb +25 -0
- data/lib/ruby_llm/agents/base_agent.rb +71 -4
- data/lib/ruby_llm/agents/core/base.rb +4 -0
- data/lib/ruby_llm/agents/core/configuration.rb +11 -0
- data/lib/ruby_llm/agents/core/instrumentation.rb +15 -19
- data/lib/ruby_llm/agents/core/version.rb +1 -1
- data/lib/ruby_llm/agents/infrastructure/alert_manager.rb +4 -4
- data/lib/ruby_llm/agents/infrastructure/budget_tracker.rb +19 -11
- data/lib/ruby_llm/agents/pipeline/builder.rb +8 -4
- data/lib/ruby_llm/agents/pipeline/context.rb +69 -1
- data/lib/ruby_llm/agents/pipeline/middleware/base.rb +58 -4
- data/lib/ruby_llm/agents/pipeline/middleware/budget.rb +21 -17
- data/lib/ruby_llm/agents/pipeline/middleware/cache.rb +40 -26
- data/lib/ruby_llm/agents/pipeline/middleware/instrumentation.rb +126 -120
- data/lib/ruby_llm/agents/pipeline/middleware/reliability.rb +13 -11
- data/lib/ruby_llm/agents/pipeline/middleware/tenant.rb +29 -31
- data/lib/ruby_llm/agents/providers/inception/capabilities.rb +107 -0
- data/lib/ruby_llm/agents/providers/inception/chat.rb +17 -0
- data/lib/ruby_llm/agents/providers/inception/configuration.rb +9 -0
- data/lib/ruby_llm/agents/providers/inception/models.rb +38 -0
- data/lib/ruby_llm/agents/providers/inception/registry.rb +45 -0
- data/lib/ruby_llm/agents/providers/inception.rb +50 -0
- data/lib/ruby_llm/agents/rails/engine.rb +11 -0
- data/lib/ruby_llm/agents/results/background_removal_result.rb +7 -1
- data/lib/ruby_llm/agents/results/base.rb +28 -4
- data/lib/ruby_llm/agents/results/embedding_result.rb +4 -0
- data/lib/ruby_llm/agents/results/image_analysis_result.rb +11 -3
- data/lib/ruby_llm/agents/results/image_edit_result.rb +7 -1
- data/lib/ruby_llm/agents/results/image_generation_result.rb +7 -1
- data/lib/ruby_llm/agents/results/image_pipeline_result.rb +7 -1
- data/lib/ruby_llm/agents/results/image_transform_result.rb +7 -1
- data/lib/ruby_llm/agents/results/image_upscale_result.rb +7 -1
- data/lib/ruby_llm/agents/results/image_variation_result.rb +7 -1
- data/lib/ruby_llm/agents/results/speech_result.rb +6 -0
- data/lib/ruby_llm/agents/results/trackable.rb +25 -0
- data/lib/ruby_llm/agents/results/transcription_result.rb +6 -0
- data/lib/ruby_llm/agents/text/embedder.rb +8 -1
- data/lib/ruby_llm/agents/track_report.rb +127 -0
- data/lib/ruby_llm/agents/tracker.rb +32 -0
- data/lib/ruby_llm/agents.rb +212 -0
- data/lib/tasks/ruby_llm_agents.rake +6 -0
- metadata +17 -2
|
@@ -46,6 +46,9 @@ module RubyLLM
|
|
|
46
46
|
# Response metadata
|
|
47
47
|
attr_accessor :model_used, :finish_reason, :time_to_first_token_ms
|
|
48
48
|
|
|
49
|
+
# Debug trace (set when debug: true is passed)
|
|
50
|
+
attr_accessor :trace
|
|
51
|
+
|
|
49
52
|
# Streaming support
|
|
50
53
|
attr_accessor :stream_block, :skip_cache
|
|
51
54
|
|
|
@@ -85,6 +88,10 @@ module RubyLLM
|
|
|
85
88
|
@skip_cache = skip_cache
|
|
86
89
|
@stream_block = stream_block
|
|
87
90
|
|
|
91
|
+
# Debug trace
|
|
92
|
+
@trace = []
|
|
93
|
+
@trace_enabled = options[:debug] == true
|
|
94
|
+
|
|
88
95
|
# Initialize tracking fields
|
|
89
96
|
@attempt = 0
|
|
90
97
|
@attempts_made = 0
|
|
@@ -108,6 +115,23 @@ module RubyLLM
|
|
|
108
115
|
((@completed_at - @started_at) * 1000).to_i
|
|
109
116
|
end
|
|
110
117
|
|
|
118
|
+
# Is debug tracing enabled?
|
|
119
|
+
#
|
|
120
|
+
# @return [Boolean]
|
|
121
|
+
def trace_enabled?
|
|
122
|
+
@trace_enabled
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# Adds a trace entry for a middleware execution
|
|
126
|
+
#
|
|
127
|
+
# @param middleware_name [String] Name of the middleware
|
|
128
|
+
# @param started_at [Time] When the middleware started
|
|
129
|
+
# @param duration_ms [Float] How long the middleware took in ms
|
|
130
|
+
# @param action [String, nil] Optional action description (e.g., "cache hit")
|
|
131
|
+
def add_trace(middleware_name, started_at:, duration_ms:, action: nil)
|
|
132
|
+
@trace << {middleware: middleware_name, started_at: started_at, duration_ms: duration_ms, action: action}.compact
|
|
133
|
+
end
|
|
134
|
+
|
|
111
135
|
# Was the result served from cache?
|
|
112
136
|
#
|
|
113
137
|
# @return [Boolean]
|
|
@@ -136,6 +160,24 @@ module RubyLLM
|
|
|
136
160
|
(@input_tokens || 0) + (@output_tokens || 0)
|
|
137
161
|
end
|
|
138
162
|
|
|
163
|
+
# Returns a RubyLLM interface scoped to tenant API keys when present.
|
|
164
|
+
#
|
|
165
|
+
# When tenant API keys are stored on this context (by the Tenant middleware),
|
|
166
|
+
# returns a RubyLLM::Context with a cloned config that has tenant-specific
|
|
167
|
+
# keys applied. This avoids mutating global RubyLLM configuration, making
|
|
168
|
+
# multi-tenant LLM calls thread-safe.
|
|
169
|
+
#
|
|
170
|
+
# When no tenant API keys are present, returns the RubyLLM module directly
|
|
171
|
+
# (which uses the global configuration).
|
|
172
|
+
#
|
|
173
|
+
# @return [RubyLLM::Context, RubyLLM] Scoped context or global module
|
|
174
|
+
def llm
|
|
175
|
+
api_keys = self[:tenant_api_keys]
|
|
176
|
+
return RubyLLM if api_keys.nil? || api_keys.empty?
|
|
177
|
+
|
|
178
|
+
@llm_context ||= build_llm_context(api_keys)
|
|
179
|
+
end
|
|
180
|
+
|
|
139
181
|
# Custom metadata storage - read
|
|
140
182
|
#
|
|
141
183
|
# @param key [Symbol, String] The metadata key
|
|
@@ -212,11 +254,31 @@ module RubyLLM
|
|
|
212
254
|
# Preserve execution hierarchy
|
|
213
255
|
new_ctx.parent_execution_id = @parent_execution_id
|
|
214
256
|
new_ctx.root_execution_id = @root_execution_id
|
|
257
|
+
# Preserve trace across retries
|
|
258
|
+
new_ctx.trace = @trace
|
|
215
259
|
new_ctx
|
|
216
260
|
end
|
|
217
261
|
|
|
218
262
|
private
|
|
219
263
|
|
|
264
|
+
# Builds a RubyLLM::Context with tenant-specific API keys
|
|
265
|
+
#
|
|
266
|
+
# Clones the global RubyLLM config and overlays tenant API keys,
|
|
267
|
+
# then wraps it in a RubyLLM::Context for thread-safe per-request use.
|
|
268
|
+
#
|
|
269
|
+
# @param api_keys [Hash] Provider => key mappings (e.g., {openai: "sk-..."})
|
|
270
|
+
# @return [RubyLLM::Context] Context with tenant-scoped configuration
|
|
271
|
+
def build_llm_context(api_keys)
|
|
272
|
+
config = RubyLLM.config.dup
|
|
273
|
+
api_keys.each do |provider, key|
|
|
274
|
+
next if key.nil? || (key.respond_to?(:empty?) && key.empty?)
|
|
275
|
+
|
|
276
|
+
setter = "#{provider}_api_key="
|
|
277
|
+
config.public_send(setter, key) if config.respond_to?(setter)
|
|
278
|
+
end
|
|
279
|
+
RubyLLM::Context.new(config)
|
|
280
|
+
end
|
|
281
|
+
|
|
220
282
|
# Extracts agent_type from the agent class
|
|
221
283
|
#
|
|
222
284
|
# @param agent_class [Class] The agent class
|
|
@@ -227,7 +289,13 @@ module RubyLLM
|
|
|
227
289
|
if agent_class.respond_to?(:agent_type)
|
|
228
290
|
agent_class.agent_type
|
|
229
291
|
else
|
|
230
|
-
|
|
292
|
+
if defined?(RubyLLM::Agents::Deprecations)
|
|
293
|
+
RubyLLM::Agents::Deprecations.warn(
|
|
294
|
+
"#{agent_class.name || agent_class} does not define `agent_type`. " \
|
|
295
|
+
"Guessing from class name. Define `self.agent_type` to silence this warning.",
|
|
296
|
+
caller
|
|
297
|
+
)
|
|
298
|
+
end
|
|
231
299
|
infer_agent_type(agent_class)
|
|
232
300
|
end
|
|
233
301
|
end
|
|
@@ -41,6 +41,8 @@ module RubyLLM
|
|
|
41
41
|
# @abstract Subclass and implement {#call}
|
|
42
42
|
#
|
|
43
43
|
class Base
|
|
44
|
+
LOG_TAG = "[RubyLLM::Agents::Pipeline]"
|
|
45
|
+
|
|
44
46
|
# @param app [#call] The next handler in the chain
|
|
45
47
|
# @param agent_class [Class] The agent class (for reading DSL config)
|
|
46
48
|
def initialize(app, agent_class)
|
|
@@ -100,22 +102,74 @@ module RubyLLM
|
|
|
100
102
|
RubyLLM::Agents.configuration
|
|
101
103
|
end
|
|
102
104
|
|
|
105
|
+
# Builds a log prefix with context from the execution
|
|
106
|
+
#
|
|
107
|
+
# Includes agent type, execution ID, and tenant when available
|
|
108
|
+
# so log messages can be traced through the full pipeline.
|
|
109
|
+
#
|
|
110
|
+
# @param context [Context, nil] The execution context
|
|
111
|
+
# @return [String] Formatted log prefix
|
|
112
|
+
def log_prefix(context = nil)
|
|
113
|
+
return LOG_TAG unless context
|
|
114
|
+
|
|
115
|
+
parts = [LOG_TAG]
|
|
116
|
+
parts << context.agent_class.name if context.agent_class
|
|
117
|
+
parts << "exec=#{context.execution_id}" if context.execution_id
|
|
118
|
+
parts << "tenant=#{context.tenant_id}" if context.tenant_id
|
|
119
|
+
parts.join(" ")
|
|
120
|
+
end
|
|
121
|
+
|
|
103
122
|
# Log a debug message if Rails logger is available
|
|
104
123
|
#
|
|
105
124
|
# @param message [String] The message to log
|
|
106
|
-
|
|
125
|
+
# @param context [Context, nil] Optional execution context for structured prefix
|
|
126
|
+
def debug(message, context = nil)
|
|
107
127
|
return unless defined?(Rails) && Rails.logger
|
|
108
128
|
|
|
109
|
-
Rails.logger.debug("
|
|
129
|
+
Rails.logger.debug("#{log_prefix(context)} #{message}")
|
|
110
130
|
end
|
|
111
131
|
|
|
112
132
|
# Log an error message if Rails logger is available
|
|
113
133
|
#
|
|
114
134
|
# @param message [String] The message to log
|
|
115
|
-
|
|
135
|
+
# @param context [Context, nil] Optional execution context for structured prefix
|
|
136
|
+
def error(message, context = nil)
|
|
137
|
+
return unless defined?(Rails) && Rails.logger
|
|
138
|
+
|
|
139
|
+
Rails.logger.error("#{log_prefix(context)} #{message}")
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# Traces middleware execution when debug mode is enabled.
|
|
143
|
+
#
|
|
144
|
+
# Wraps a block with timing instrumentation. When tracing is not
|
|
145
|
+
# enabled, yields directly with zero overhead.
|
|
146
|
+
#
|
|
147
|
+
# @param context [Context] The execution context
|
|
148
|
+
# @param action [String, nil] Optional action description
|
|
149
|
+
# @yield The block to trace
|
|
150
|
+
# @return [Object] The block's return value
|
|
151
|
+
def trace(context, action: nil)
|
|
152
|
+
unless context.trace_enabled?
|
|
153
|
+
return yield
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
middleware_name = self.class.name&.split("::")&.last || self.class.to_s
|
|
157
|
+
started_at = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
158
|
+
result = yield
|
|
159
|
+
duration_ms = ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - started_at) * 1000).round(2)
|
|
160
|
+
context.add_trace(middleware_name, started_at: Time.current, duration_ms: duration_ms, action: action)
|
|
161
|
+
debug("#{middleware_name} completed in #{duration_ms}ms#{" (#{action})" if action}", context)
|
|
162
|
+
result
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
# Log a warning message if Rails logger is available
|
|
166
|
+
#
|
|
167
|
+
# @param message [String] The message to log
|
|
168
|
+
# @param context [Context, nil] Optional execution context for structured prefix
|
|
169
|
+
def warn(message, context = nil)
|
|
116
170
|
return unless defined?(Rails) && Rails.logger
|
|
117
171
|
|
|
118
|
-
Rails.logger.
|
|
172
|
+
Rails.logger.warn("#{log_prefix(context)} #{message}")
|
|
119
173
|
end
|
|
120
174
|
end
|
|
121
175
|
end
|
|
@@ -32,21 +32,23 @@ module RubyLLM
|
|
|
32
32
|
def call(context)
|
|
33
33
|
return @app.call(context) unless budgets_enabled?
|
|
34
34
|
|
|
35
|
-
|
|
36
|
-
|
|
35
|
+
trace(context) do
|
|
36
|
+
# Check budget before execution
|
|
37
|
+
check_budget!(context)
|
|
37
38
|
|
|
38
|
-
|
|
39
|
-
|
|
39
|
+
# Execute the chain
|
|
40
|
+
@app.call(context)
|
|
40
41
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
42
|
+
# Record spend after successful execution (if not cached)
|
|
43
|
+
if context.success? && !context.cached?
|
|
44
|
+
record_spend!(context)
|
|
45
|
+
emit_budget_notification("ruby_llm_agents.budget.record", context,
|
|
46
|
+
total_cost: context.total_cost,
|
|
47
|
+
total_tokens: context.total_tokens)
|
|
48
|
+
end
|
|
48
49
|
|
|
49
|
-
|
|
50
|
+
context
|
|
51
|
+
end
|
|
50
52
|
end
|
|
51
53
|
|
|
52
54
|
private
|
|
@@ -64,8 +66,8 @@ module RubyLLM
|
|
|
64
66
|
tenant_id: context.tenant_id
|
|
65
67
|
}.merge(extras)
|
|
66
68
|
)
|
|
67
|
-
rescue
|
|
68
|
-
|
|
69
|
+
rescue => e
|
|
70
|
+
debug("Budget notification failed: #{e.message}", context)
|
|
69
71
|
end
|
|
70
72
|
|
|
71
73
|
# Returns whether budgets are enabled globally
|
|
@@ -73,7 +75,8 @@ module RubyLLM
|
|
|
73
75
|
# @return [Boolean]
|
|
74
76
|
def budgets_enabled?
|
|
75
77
|
global_config.budgets_enabled?
|
|
76
|
-
rescue
|
|
78
|
+
rescue => e
|
|
79
|
+
debug("Failed to check budgets_enabled config: #{e.message}")
|
|
77
80
|
false
|
|
78
81
|
end
|
|
79
82
|
|
|
@@ -104,7 +107,8 @@ module RubyLLM
|
|
|
104
107
|
emit_budget_notification("ruby_llm_agents.budget.exceeded", context)
|
|
105
108
|
raise
|
|
106
109
|
rescue => e
|
|
107
|
-
error
|
|
110
|
+
# Log at error level so unexpected failures are visible in logs
|
|
111
|
+
error("Budget check failed: #{e.class}: #{e.message}", context)
|
|
108
112
|
end
|
|
109
113
|
|
|
110
114
|
# Records spend after execution
|
|
@@ -143,7 +147,7 @@ module RubyLLM
|
|
|
143
147
|
)
|
|
144
148
|
end
|
|
145
149
|
rescue => e
|
|
146
|
-
error("Failed to record spend: #{e.message}")
|
|
150
|
+
error("Failed to record spend: #{e.message}", context)
|
|
147
151
|
end
|
|
148
152
|
end
|
|
149
153
|
end
|
|
@@ -31,34 +31,46 @@ module RubyLLM
|
|
|
31
31
|
def call(context)
|
|
32
32
|
return @app.call(context) unless cache_enabled?
|
|
33
33
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
#
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
34
|
+
cache_action = nil
|
|
35
|
+
result = trace(context, action: "cache") do
|
|
36
|
+
cache_key = generate_cache_key(context)
|
|
37
|
+
|
|
38
|
+
# Skip cache read if skip_cache is true
|
|
39
|
+
unless context.skip_cache
|
|
40
|
+
# Try to read from cache
|
|
41
|
+
if (cached = cache_read(cache_key))
|
|
42
|
+
context.output = cached
|
|
43
|
+
context.cached = true
|
|
44
|
+
context[:cache_key] = cache_key
|
|
45
|
+
cache_action = "hit"
|
|
46
|
+
debug("Cache hit for #{cache_key}", context)
|
|
47
|
+
emit_cache_notification("ruby_llm_agents.cache.hit", cache_key)
|
|
48
|
+
next context
|
|
49
|
+
end
|
|
46
50
|
end
|
|
47
|
-
end
|
|
48
51
|
|
|
49
|
-
|
|
52
|
+
cache_action = "miss"
|
|
53
|
+
emit_cache_notification("ruby_llm_agents.cache.miss", cache_key)
|
|
54
|
+
|
|
55
|
+
# Execute the chain
|
|
56
|
+
@app.call(context)
|
|
57
|
+
|
|
58
|
+
# Cache successful results
|
|
59
|
+
if context.success?
|
|
60
|
+
cache_write(cache_key, context.output)
|
|
61
|
+
debug("Cache write for #{cache_key}", context)
|
|
62
|
+
emit_cache_notification("ruby_llm_agents.cache.write", cache_key)
|
|
63
|
+
end
|
|
50
64
|
|
|
51
|
-
|
|
52
|
-
|
|
65
|
+
context
|
|
66
|
+
end
|
|
53
67
|
|
|
54
|
-
#
|
|
55
|
-
if context.
|
|
56
|
-
|
|
57
|
-
debug("Cache write for #{cache_key}")
|
|
58
|
-
emit_cache_notification("ruby_llm_agents.cache.write", cache_key)
|
|
68
|
+
# Update the last trace entry with the specific cache action
|
|
69
|
+
if context.trace_enabled? && cache_action && context.trace.last
|
|
70
|
+
context.trace.last[:action] = cache_action
|
|
59
71
|
end
|
|
60
72
|
|
|
61
|
-
|
|
73
|
+
result
|
|
62
74
|
end
|
|
63
75
|
|
|
64
76
|
private
|
|
@@ -73,8 +85,8 @@ module RubyLLM
|
|
|
73
85
|
agent_type: @agent_class&.name,
|
|
74
86
|
cache_key: cache_key
|
|
75
87
|
)
|
|
76
|
-
rescue
|
|
77
|
-
|
|
88
|
+
rescue => e
|
|
89
|
+
debug("Cache notification failed: #{e.message}")
|
|
78
90
|
end
|
|
79
91
|
|
|
80
92
|
# Returns whether caching is enabled for this agent
|
|
@@ -89,7 +101,8 @@ module RubyLLM
|
|
|
89
101
|
# @return [ActiveSupport::Cache::Store, nil]
|
|
90
102
|
def cache_store
|
|
91
103
|
global_config.cache_store
|
|
92
|
-
rescue
|
|
104
|
+
rescue => e
|
|
105
|
+
debug("Failed to access cache_store config: #{e.message}")
|
|
93
106
|
nil
|
|
94
107
|
end
|
|
95
108
|
|
|
@@ -148,7 +161,8 @@ module RubyLLM
|
|
|
148
161
|
else
|
|
149
162
|
input.to_json
|
|
150
163
|
end
|
|
151
|
-
rescue
|
|
164
|
+
rescue => e
|
|
165
|
+
debug("Failed to serialize input for cache key: #{e.message}")
|
|
152
166
|
input.to_s
|
|
153
167
|
end
|
|
154
168
|
|