ruby_llm-agents 3.8.0 → 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.
Files changed (45) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +30 -10
  3. data/app/controllers/ruby_llm/agents/requests_controller.rb +117 -0
  4. data/app/views/layouts/ruby_llm/agents/application.html.erb +4 -2
  5. data/app/views/ruby_llm/agents/requests/index.html.erb +153 -0
  6. data/app/views/ruby_llm/agents/requests/show.html.erb +136 -0
  7. data/config/routes.rb +2 -0
  8. data/lib/generators/ruby_llm_agents/agent_generator.rb +2 -2
  9. data/lib/generators/ruby_llm_agents/demo_generator.rb +102 -0
  10. data/lib/generators/ruby_llm_agents/doctor_generator.rb +196 -0
  11. data/lib/generators/ruby_llm_agents/install_generator.rb +7 -19
  12. data/lib/generators/ruby_llm_agents/templates/agent.rb.tt +27 -80
  13. data/lib/generators/ruby_llm_agents/templates/application_agent.rb.tt +18 -51
  14. data/lib/generators/ruby_llm_agents/templates/initializer.rb.tt +19 -17
  15. data/lib/ruby_llm/agents/base_agent.rb +68 -7
  16. data/lib/ruby_llm/agents/core/base.rb +4 -0
  17. data/lib/ruby_llm/agents/core/configuration.rb +10 -0
  18. data/lib/ruby_llm/agents/core/version.rb +1 -1
  19. data/lib/ruby_llm/agents/pipeline/context.rb +26 -0
  20. data/lib/ruby_llm/agents/pipeline/middleware/base.rb +58 -4
  21. data/lib/ruby_llm/agents/pipeline/middleware/budget.rb +17 -15
  22. data/lib/ruby_llm/agents/pipeline/middleware/cache.rb +34 -22
  23. data/lib/ruby_llm/agents/pipeline/middleware/instrumentation.rb +105 -50
  24. data/lib/ruby_llm/agents/pipeline/middleware/reliability.rb +7 -5
  25. data/lib/ruby_llm/agents/pipeline/middleware/tenant.rb +6 -4
  26. data/lib/ruby_llm/agents/rails/engine.rb +11 -0
  27. data/lib/ruby_llm/agents/results/background_removal_result.rb +7 -1
  28. data/lib/ruby_llm/agents/results/base.rb +24 -2
  29. data/lib/ruby_llm/agents/results/embedding_result.rb +4 -0
  30. data/lib/ruby_llm/agents/results/image_analysis_result.rb +7 -1
  31. data/lib/ruby_llm/agents/results/image_edit_result.rb +7 -1
  32. data/lib/ruby_llm/agents/results/image_generation_result.rb +7 -1
  33. data/lib/ruby_llm/agents/results/image_pipeline_result.rb +7 -1
  34. data/lib/ruby_llm/agents/results/image_transform_result.rb +7 -1
  35. data/lib/ruby_llm/agents/results/image_upscale_result.rb +7 -1
  36. data/lib/ruby_llm/agents/results/image_variation_result.rb +7 -1
  37. data/lib/ruby_llm/agents/results/speech_result.rb +6 -0
  38. data/lib/ruby_llm/agents/results/trackable.rb +25 -0
  39. data/lib/ruby_llm/agents/results/transcription_result.rb +6 -0
  40. data/lib/ruby_llm/agents/text/embedder.rb +7 -4
  41. data/lib/ruby_llm/agents/track_report.rb +127 -0
  42. data/lib/ruby_llm/agents/tracker.rb +32 -0
  43. data/lib/ruby_llm/agents.rb +208 -0
  44. data/lib/tasks/ruby_llm_agents.rake +6 -0
  45. metadata +10 -2
@@ -107,6 +107,16 @@ module RubyLLM
107
107
  # @return [Integer, nil] Database ID of the associated Execution record
108
108
  attr_reader :execution_id
109
109
 
110
+ # @!group Tracking
111
+ # @!attribute [r] agent_class_name
112
+ # @return [String, nil] The agent class that produced this result
113
+ attr_reader :agent_class_name
114
+
115
+ # @!group Debug
116
+ # @!attribute [r] trace
117
+ # @return [Array<Hash>, nil] Pipeline trace entries (when debug: true)
118
+ attr_reader :trace
119
+
110
120
  # Creates a new Result instance
111
121
  #
112
122
  # @param content [Hash, String] The processed response content
@@ -159,6 +169,16 @@ module RubyLLM
159
169
 
160
170
  # Execution record
161
171
  @execution_id = options[:execution_id]
172
+
173
+ # Tracking
174
+ @agent_class_name = options[:agent_class_name]
175
+
176
+ # Debug trace
177
+ @trace = options[:trace]
178
+
179
+ # Register with active tracker
180
+ tracker = Thread.current[:ruby_llm_agents_tracker]
181
+ tracker << self if tracker
162
182
  end
163
183
 
164
184
  # Loads the associated Execution record from the database
@@ -264,8 +284,10 @@ module RubyLLM
264
284
  thinking_text: thinking_text,
265
285
  thinking_signature: thinking_signature,
266
286
  thinking_tokens: thinking_tokens,
267
- execution_id: execution_id
268
- }
287
+ execution_id: execution_id,
288
+ agent_class_name: agent_class_name,
289
+ trace: trace
290
+ }.compact
269
291
  end
270
292
 
271
293
  # @!group Deprecated Hash Delegation
@@ -25,6 +25,8 @@ module RubyLLM
25
25
  #
26
26
  # @api public
27
27
  class EmbeddingResult
28
+ include Trackable
29
+
28
30
  # @!attribute [r] vectors
29
31
  # @return [Array<Array<Float>>] The embedding vectors
30
32
  attr_reader :vectors
@@ -106,6 +108,8 @@ module RubyLLM
106
108
  @error_class = attributes[:error_class]
107
109
  @error_message = attributes[:error_message]
108
110
  @execution_id = attributes[:execution_id]
111
+ @agent_class_name = attributes[:agent_class_name]
112
+ register_with_tracker
109
113
  end
110
114
 
111
115
  # Loads the associated Execution record from the database
@@ -17,6 +17,8 @@ module RubyLLM
17
17
  # result.success? # => true
18
18
  #
19
19
  class ImageAnalysisResult
20
+ include Trackable
21
+
20
22
  attr_reader :image, :model_id, :analysis_type,
21
23
  :caption, :description, :tags, :objects, :colors, :text,
22
24
  :raw_response, :started_at, :completed_at, :tenant_id, :analyzer_class,
@@ -43,7 +45,7 @@ module RubyLLM
43
45
  # @param error_message [String, nil] Error message if failed
44
46
  def initialize(image:, model_id:, analysis_type:, caption:, description:, tags:,
45
47
  objects:, colors:, text:, raw_response:, started_at:, completed_at:,
46
- tenant_id:, analyzer_class:, error_class: nil, error_message: nil)
48
+ tenant_id:, analyzer_class:, error_class: nil, error_message: nil, agent_class_name: nil)
47
49
  @image = image
48
50
  @model_id = model_id
49
51
  @analysis_type = analysis_type
@@ -61,6 +63,10 @@ module RubyLLM
61
63
  @error_class = error_class
62
64
  @error_message = error_message
63
65
  @execution_id = nil
66
+
67
+ # Tracking
68
+ @agent_class_name = agent_class_name
69
+ register_with_tracker
64
70
  end
65
71
 
66
72
  # Loads the associated Execution record from the database
@@ -17,6 +17,8 @@ module RubyLLM
17
17
  # result.success? # => true
18
18
  #
19
19
  class ImageEditResult
20
+ include Trackable
21
+
20
22
  attr_reader :images, :source_image, :mask, :prompt, :model_id, :size,
21
23
  :started_at, :completed_at, :tenant_id, :editor_class,
22
24
  :error_class, :error_message
@@ -38,7 +40,7 @@ module RubyLLM
38
40
  # @param error_message [String, nil] Error message if failed
39
41
  def initialize(images:, source_image:, mask:, prompt:, model_id:, size:,
40
42
  started_at:, completed_at:, tenant_id:, editor_class:,
41
- error_class: nil, error_message: nil)
43
+ error_class: nil, error_message: nil, agent_class_name: nil)
42
44
  @images = images
43
45
  @source_image = source_image
44
46
  @mask = mask
@@ -52,6 +54,10 @@ module RubyLLM
52
54
  @error_class = error_class
53
55
  @error_message = error_message
54
56
  @execution_id = nil
57
+
58
+ # Tracking
59
+ @agent_class_name = agent_class_name
60
+ register_with_tracker
55
61
  end
56
62
 
57
63
  # Loads the associated Execution record from the database
@@ -20,6 +20,8 @@ module RubyLLM
20
20
  # result.save_all("./logos")
21
21
  #
22
22
  class ImageGenerationResult
23
+ include Trackable
24
+
23
25
  attr_reader :images, :prompt, :model_id, :size, :quality, :style,
24
26
  :started_at, :completed_at, :tenant_id, :generator_class,
25
27
  :error_class, :error_message, :execution_id
@@ -40,7 +42,7 @@ module RubyLLM
40
42
  # @param error_message [String, nil] Error message if failed
41
43
  def initialize(images:, prompt:, model_id:, size:, quality:, style:,
42
44
  started_at:, completed_at:, tenant_id:, generator_class:,
43
- error_class: nil, error_message: nil, execution_id: nil)
45
+ error_class: nil, error_message: nil, execution_id: nil, agent_class_name: nil)
44
46
  @images = images
45
47
  @prompt = prompt
46
48
  @model_id = model_id
@@ -54,6 +56,10 @@ module RubyLLM
54
56
  @error_class = error_class
55
57
  @error_message = error_message
56
58
  @execution_id = execution_id
59
+
60
+ # Tracking
61
+ @agent_class_name = agent_class_name
62
+ register_with_tracker
57
63
  end
58
64
 
59
65
  # Loads the associated Execution record from the database
@@ -21,6 +21,8 @@ module RubyLLM
21
21
  # result.analysis # => Shortcut to analyzer step result
22
22
  #
23
23
  class ImagePipelineResult
24
+ include Trackable
25
+
24
26
  attr_reader :step_results, :started_at, :completed_at, :tenant_id,
25
27
  :pipeline_class, :context, :error_class, :error_message
26
28
  attr_accessor :execution_id
@@ -36,7 +38,7 @@ module RubyLLM
36
38
  # @param error_class [String, nil] Error class name if failed
37
39
  # @param error_message [String, nil] Error message if failed
38
40
  def initialize(step_results:, started_at:, completed_at:, tenant_id:,
39
- pipeline_class:, context:, error_class: nil, error_message: nil)
41
+ pipeline_class:, context:, error_class: nil, error_message: nil, agent_class_name: nil)
40
42
  @step_results = step_results
41
43
  @started_at = started_at
42
44
  @completed_at = completed_at
@@ -46,6 +48,10 @@ module RubyLLM
46
48
  @error_class = error_class
47
49
  @error_message = error_message
48
50
  @execution_id = nil
51
+
52
+ # Tracking
53
+ @agent_class_name = agent_class_name
54
+ register_with_tracker
49
55
  end
50
56
 
51
57
  # Loads the associated Execution record from the database
@@ -17,6 +17,8 @@ module RubyLLM
17
17
  # result.success? # => true
18
18
  #
19
19
  class ImageTransformResult
20
+ include Trackable
21
+
20
22
  attr_reader :images, :source_image, :prompt, :model_id, :size, :strength,
21
23
  :started_at, :completed_at, :tenant_id, :transformer_class,
22
24
  :error_class, :error_message
@@ -38,7 +40,7 @@ module RubyLLM
38
40
  # @param error_message [String, nil] Error message if failed
39
41
  def initialize(images:, source_image:, prompt:, model_id:, size:, strength:,
40
42
  started_at:, completed_at:, tenant_id:, transformer_class:,
41
- error_class: nil, error_message: nil)
43
+ error_class: nil, error_message: nil, agent_class_name: nil)
42
44
  @images = images
43
45
  @source_image = source_image
44
46
  @prompt = prompt
@@ -52,6 +54,10 @@ module RubyLLM
52
54
  @error_class = error_class
53
55
  @error_message = error_message
54
56
  @execution_id = nil
57
+
58
+ # Tracking
59
+ @agent_class_name = agent_class_name
60
+ register_with_tracker
55
61
  end
56
62
 
57
63
  # Loads the associated Execution record from the database
@@ -15,6 +15,8 @@ module RubyLLM
15
15
  # result.success? # => true
16
16
  #
17
17
  class ImageUpscaleResult
18
+ include Trackable
19
+
18
20
  attr_reader :image, :source_image, :model_id, :scale, :output_size, :face_enhance,
19
21
  :started_at, :completed_at, :tenant_id, :upscaler_class,
20
22
  :error_class, :error_message
@@ -36,7 +38,7 @@ module RubyLLM
36
38
  # @param error_message [String, nil] Error message if failed
37
39
  def initialize(image:, source_image:, model_id:, scale:, output_size:, face_enhance:,
38
40
  started_at:, completed_at:, tenant_id:, upscaler_class:,
39
- error_class: nil, error_message: nil)
41
+ error_class: nil, error_message: nil, agent_class_name: nil)
40
42
  @image = image
41
43
  @source_image = source_image
42
44
  @model_id = model_id
@@ -50,6 +52,10 @@ module RubyLLM
50
52
  @error_class = error_class
51
53
  @error_message = error_message
52
54
  @execution_id = nil
55
+
56
+ # Tracking
57
+ @agent_class_name = agent_class_name
58
+ register_with_tracker
53
59
  end
54
60
 
55
61
  # Loads the associated Execution record from the database
@@ -14,6 +14,8 @@ module RubyLLM
14
14
  # result.success? # => true
15
15
  #
16
16
  class ImageVariationResult
17
+ include Trackable
18
+
17
19
  attr_reader :images, :source_image, :model_id, :size, :variation_strength,
18
20
  :started_at, :completed_at, :tenant_id, :variator_class,
19
21
  :error_class, :error_message
@@ -34,7 +36,7 @@ module RubyLLM
34
36
  # @param error_message [String, nil] Error message if failed
35
37
  def initialize(images:, source_image:, model_id:, size:, variation_strength:,
36
38
  started_at:, completed_at:, tenant_id:, variator_class:,
37
- error_class: nil, error_message: nil)
39
+ error_class: nil, error_message: nil, agent_class_name: nil)
38
40
  @images = images
39
41
  @source_image = source_image
40
42
  @model_id = model_id
@@ -47,6 +49,10 @@ module RubyLLM
47
49
  @error_class = error_class
48
50
  @error_message = error_message
49
51
  @execution_id = nil
52
+
53
+ # Tracking
54
+ @agent_class_name = agent_class_name
55
+ register_with_tracker
50
56
  end
51
57
 
52
58
  # Loads the associated Execution record from the database
@@ -23,6 +23,8 @@ module RubyLLM
23
23
  #
24
24
  # @api public
25
25
  class SpeechResult
26
+ include Trackable
27
+
26
28
  # @!group Audio Content
27
29
 
28
30
  # @!attribute [r] audio
@@ -229,6 +231,10 @@ module RubyLLM
229
231
 
230
232
  # Execution record
231
233
  @execution_id = attributes[:execution_id]
234
+
235
+ # Tracking
236
+ @agent_class_name = attributes[:agent_class_name]
237
+ register_with_tracker
232
238
  end
233
239
 
234
240
  # Loads the associated Execution record from the database
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module Agents
5
+ # Mixin that registers a result object with the active Tracker.
6
+ #
7
+ # Included in every result class so that RubyLLM::Agents.track
8
+ # can collect results automatically.
9
+ #
10
+ # @api private
11
+ module Trackable
12
+ def self.included(base)
13
+ base.attr_reader :agent_class_name unless base.method_defined?(:agent_class_name)
14
+ end
15
+
16
+ private
17
+
18
+ # Call from the end of initialize to register with the active tracker
19
+ def register_with_tracker
20
+ tracker = Thread.current[:ruby_llm_agents_tracker]
21
+ tracker << self if tracker
22
+ end
23
+ end
24
+ end
25
+ end
@@ -26,6 +26,8 @@ module RubyLLM
26
26
  #
27
27
  # @api public
28
28
  class TranscriptionResult
29
+ include Trackable
30
+
29
31
  # @!group Content
30
32
 
31
33
  # @!attribute [r] text
@@ -250,6 +252,10 @@ module RubyLLM
250
252
 
251
253
  # Execution record
252
254
  @execution_id = attributes[:execution_id]
255
+
256
+ # Tracking
257
+ @agent_class_name = attributes[:agent_class_name]
258
+ register_with_tracker
253
259
  end
254
260
 
255
261
  # Loads the associated Execution record from the database
@@ -337,11 +337,14 @@ module RubyLLM
337
337
  embed_options = {model: context&.model || resolved_model}
338
338
  embed_options[:dimensions] = resolved_dimensions if resolved_dimensions
339
339
 
340
- # Pass scoped RubyLLM context for thread-safe per-tenant API keys
340
+ # Use scoped RubyLLM::Context for thread-safe per-tenant API keys.
341
+ # RubyLLM::Context#embed creates an Embedding with the scoped config.
341
342
  llm_ctx = context&.llm
342
- embed_options[:context] = llm_ctx if llm_ctx.is_a?(RubyLLM::Context)
343
-
344
- response = RubyLLM.embed(preprocessed, **embed_options)
343
+ response = if llm_ctx.is_a?(RubyLLM::Context)
344
+ llm_ctx.embed(preprocessed, **embed_options)
345
+ else
346
+ RubyLLM.embed(preprocessed, **embed_options)
347
+ end
345
348
 
346
349
  # ruby_llm returns vectors as an array (even for single text)
347
350
  vectors = response.vectors
@@ -0,0 +1,127 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module Agents
5
+ # Aggregated read-only report returned by RubyLLM::Agents.track.
6
+ #
7
+ # Provides totals and breakdowns across all agent calls made
8
+ # inside the tracked block.
9
+ #
10
+ # @example
11
+ # report = RubyLLM::Agents.track do
12
+ # TranscribeAgent.call(with: audio_path)
13
+ # ChatAgent.call(message: "hello")
14
+ # end
15
+ # report.total_cost # => 0.0078
16
+ # report.call_count # => 2
17
+ #
18
+ # @api public
19
+ class TrackReport
20
+ attr_reader :value, :error, :results, :request_id
21
+ attr_reader :started_at, :completed_at
22
+
23
+ def initialize(value:, error:, results:, request_id:, started_at:, completed_at:)
24
+ @value = value
25
+ @error = error
26
+ @results = results.freeze
27
+ @request_id = request_id
28
+ @started_at = started_at
29
+ @completed_at = completed_at
30
+ end
31
+
32
+ def successful?
33
+ @error.nil?
34
+ end
35
+
36
+ def failed?
37
+ !successful?
38
+ end
39
+
40
+ def call_count
41
+ @results.size
42
+ end
43
+
44
+ def total_cost
45
+ @results.sum { |r| r.total_cost || 0 }
46
+ end
47
+
48
+ def input_cost
49
+ @results.sum { |r| r.input_cost || 0 }
50
+ end
51
+
52
+ def output_cost
53
+ @results.sum { |r| r.output_cost || 0 }
54
+ end
55
+
56
+ def total_tokens
57
+ @results.sum { |r| r.total_tokens }
58
+ end
59
+
60
+ def input_tokens
61
+ @results.sum { |r| r.input_tokens || 0 }
62
+ end
63
+
64
+ def output_tokens
65
+ @results.sum { |r| r.output_tokens || 0 }
66
+ end
67
+
68
+ def duration_ms
69
+ return nil unless @started_at && @completed_at
70
+ ((@completed_at - @started_at) * 1000).to_i
71
+ end
72
+
73
+ def all_successful?
74
+ @results.all?(&:success?)
75
+ end
76
+
77
+ def any_errors?
78
+ @results.any?(&:error?)
79
+ end
80
+
81
+ def errors
82
+ @results.select(&:error?)
83
+ end
84
+
85
+ def successful
86
+ @results.select(&:success?)
87
+ end
88
+
89
+ def models_used
90
+ @results.filter_map(&:chosen_model_id).uniq
91
+ end
92
+
93
+ def cost_breakdown
94
+ @results.map do |r|
95
+ {
96
+ agent: r.respond_to?(:agent_class_name) ? r.agent_class_name : nil,
97
+ model: r.chosen_model_id,
98
+ cost: r.total_cost || 0,
99
+ tokens: r.total_tokens,
100
+ duration_ms: r.duration_ms
101
+ }
102
+ end
103
+ end
104
+
105
+ def to_h
106
+ {
107
+ successful: successful?,
108
+ value: value,
109
+ error: error&.message,
110
+ request_id: request_id,
111
+ call_count: call_count,
112
+ total_cost: total_cost,
113
+ input_cost: input_cost,
114
+ output_cost: output_cost,
115
+ total_tokens: total_tokens,
116
+ input_tokens: input_tokens,
117
+ output_tokens: output_tokens,
118
+ duration_ms: duration_ms,
119
+ started_at: started_at,
120
+ completed_at: completed_at,
121
+ models_used: models_used,
122
+ cost_breakdown: cost_breakdown
123
+ }
124
+ end
125
+ end
126
+ end
127
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module Agents
5
+ # Internal collector used by RubyLLM::Agents.track to accumulate
6
+ # Result objects produced during a tracked block.
7
+ #
8
+ # Not part of the public API — users interact with TrackReport instead.
9
+ #
10
+ # @api private
11
+ class Tracker
12
+ attr_reader :results, :defaults, :request_id, :tags
13
+
14
+ def initialize(defaults: {}, request_id: nil, tags: {})
15
+ @results = []
16
+ @defaults = defaults
17
+ @request_id = request_id || generate_request_id
18
+ @tags = tags
19
+ end
20
+
21
+ def <<(result)
22
+ @results << result
23
+ end
24
+
25
+ private
26
+
27
+ def generate_request_id
28
+ "track_#{SecureRandom.hex(8)}"
29
+ end
30
+ end
31
+ end
32
+ end