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
@@ -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]
@@ -230,6 +254,8 @@ module RubyLLM
230
254
  # Preserve execution hierarchy
231
255
  new_ctx.parent_execution_id = @parent_execution_id
232
256
  new_ctx.root_execution_id = @root_execution_id
257
+ # Preserve trace across retries
258
+ new_ctx.trace = @trace
233
259
  new_ctx
234
260
  end
235
261
 
@@ -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
- def debug(message)
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("[RubyLLM::Agents::Pipeline] #{message}")
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
- def error(message)
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.error("[RubyLLM::Agents::Pipeline] #{message}")
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
- # Check budget before execution
36
- check_budget!(context)
35
+ trace(context) do
36
+ # Check budget before execution
37
+ check_budget!(context)
37
38
 
38
- # Execute the chain
39
- @app.call(context)
39
+ # Execute the chain
40
+ @app.call(context)
40
41
 
41
- # Record spend after successful execution (if not cached)
42
- if context.success? && !context.cached?
43
- record_spend!(context)
44
- emit_budget_notification("ruby_llm_agents.budget.record", context,
45
- total_cost: context.total_cost,
46
- total_tokens: context.total_tokens)
47
- end
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
- context
50
+ context
51
+ end
50
52
  end
51
53
 
52
54
  private
@@ -65,7 +67,7 @@ module RubyLLM
65
67
  }.merge(extras)
66
68
  )
67
69
  rescue => e
68
- debug("Budget notification failed: #{e.message}")
70
+ debug("Budget notification failed: #{e.message}", context)
69
71
  end
70
72
 
71
73
  # Returns whether budgets are enabled globally
@@ -106,7 +108,7 @@ module RubyLLM
106
108
  raise
107
109
  rescue => e
108
110
  # Log at error level so unexpected failures are visible in logs
109
- error("Budget check failed: #{e.class}: #{e.message}")
111
+ error("Budget check failed: #{e.class}: #{e.message}", context)
110
112
  end
111
113
 
112
114
  # Records spend after execution
@@ -145,7 +147,7 @@ module RubyLLM
145
147
  )
146
148
  end
147
149
  rescue => e
148
- error("Failed to record spend: #{e.message}")
150
+ error("Failed to record spend: #{e.message}", context)
149
151
  end
150
152
  end
151
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
- cache_key = generate_cache_key(context)
35
-
36
- # Skip cache read if skip_cache is true
37
- unless context.skip_cache
38
- # Try to read from cache
39
- if (cached = cache_read(cache_key))
40
- context.output = cached
41
- context.cached = true
42
- context[:cache_key] = cache_key
43
- debug("Cache hit for #{cache_key}")
44
- emit_cache_notification("ruby_llm_agents.cache.hit", cache_key)
45
- return context
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
- emit_cache_notification("ruby_llm_agents.cache.miss", cache_key)
52
+ cache_action = "miss"
53
+ emit_cache_notification("ruby_llm_agents.cache.miss", cache_key)
54
+
55
+ # Execute the chain
56
+ @app.call(context)
50
57
 
51
- # Execute the chain
52
- @app.call(context)
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
64
+
65
+ context
66
+ end
53
67
 
54
- # Cache successful results
55
- if context.success?
56
- cache_write(cache_key, context.output)
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
- context
73
+ result
62
74
  end
63
75
 
64
76
  private
@@ -39,45 +39,47 @@ module RubyLLM
39
39
  def call(context)
40
40
  context.started_at = Time.current
41
41
 
42
- # Create "running" record immediately (SYNC - must appear on dashboard)
43
- execution = create_running_execution(context)
44
- context.execution_id = execution&.id
45
- emit_start_notification(context)
46
- status_update_completed = false
47
- raised_exception = nil
48
-
49
- begin
50
- @app.call(context)
51
- context.completed_at = Time.current
52
-
53
- begin
54
- complete_execution(execution, context, status: "success")
55
- status_update_completed = true
56
- rescue
57
- # Let ensure block handle via mark_execution_failed!
58
- end
59
-
60
- emit_complete_notification(context, "success")
61
- rescue => e
62
- context.completed_at = Time.current
63
- context.error = e
64
- raised_exception = e
42
+ trace(context) do
43
+ # Create "running" record immediately (SYNC - must appear on dashboard)
44
+ execution = create_running_execution(context)
45
+ context.execution_id = execution&.id
46
+ emit_start_notification(context)
47
+ status_update_completed = false
48
+ raised_exception = nil
65
49
 
66
50
  begin
67
- complete_execution(execution, context, status: determine_error_status(e))
68
- status_update_completed = true
69
- rescue
70
- # Let ensure block handle via mark_execution_failed!
51
+ @app.call(context)
52
+ context.completed_at = Time.current
53
+
54
+ begin
55
+ complete_execution(execution, context, status: "success")
56
+ status_update_completed = true
57
+ rescue
58
+ # Let ensure block handle via mark_execution_failed!
59
+ end
60
+
61
+ emit_complete_notification(context, "success")
62
+ rescue => e
63
+ context.completed_at = Time.current
64
+ context.error = e
65
+ raised_exception = e
66
+
67
+ begin
68
+ complete_execution(execution, context, status: determine_error_status(e))
69
+ status_update_completed = true
70
+ rescue
71
+ # Let ensure block handle via mark_execution_failed!
72
+ end
73
+
74
+ emit_complete_notification(context, determine_error_status(e))
75
+ raise
76
+ ensure
77
+ # Emergency fallback if update failed
78
+ mark_execution_failed!(execution, error: raised_exception || $!) unless status_update_completed
71
79
  end
72
80
 
73
- emit_complete_notification(context, determine_error_status(e))
74
- raise
75
- ensure
76
- # Emergency fallback if update failed
77
- mark_execution_failed!(execution, error: raised_exception || $!) unless status_update_completed
81
+ context
78
82
  end
79
-
80
- context
81
83
  end
82
84
 
83
85
  private
@@ -111,7 +113,7 @@ module RubyLLM
111
113
 
112
114
  execution
113
115
  rescue => e
114
- error("Failed to create running execution record: #{e.message}")
116
+ error("Failed to create running execution record: #{e.message}", context)
115
117
  nil
116
118
  end
117
119
 
@@ -142,7 +144,7 @@ module RubyLLM
142
144
  # Save detail data (prompts, responses, tool calls, etc.)
143
145
  save_execution_details(execution, context, status)
144
146
  rescue => e
145
- error("Failed to complete execution record: #{e.message}")
147
+ error("Failed to complete execution record: #{e.message}", context)
146
148
  raise # Re-raise for ensure block to handle via mark_execution_failed!
147
149
  end
148
150
 
@@ -158,7 +160,7 @@ module RubyLLM
158
160
  return unless execution&.id
159
161
  return unless execution.status == "running"
160
162
 
161
- error_message = error ? "#{error.class}: #{error.message}".truncate(1000) : "Unknown error"
163
+ error_message = build_error_message(error)
162
164
 
163
165
  update_data = {
164
166
  status: "error",
@@ -183,6 +185,28 @@ module RubyLLM
183
185
  error("CRITICAL: Failed emergency status update for execution #{execution&.id}: #{e.message}")
184
186
  end
185
187
 
188
+ # Builds an informative error message including backtrace context
189
+ #
190
+ # Preserves the error class, message, and the most relevant
191
+ # backtrace frames (up to 10) so developers can trace the
192
+ # failure origin without needing to reproduce it.
193
+ #
194
+ # @param error [Exception, nil] The exception
195
+ # @return [String] Formatted error message with backtrace
196
+ def build_error_message(error)
197
+ return "Unknown error" unless error
198
+
199
+ parts = ["#{error.class}: #{error.message}"]
200
+
201
+ if error.backtrace&.any?
202
+ relevant_frames = error.backtrace.first(10)
203
+ parts << "Backtrace (first #{relevant_frames.size} frames):"
204
+ parts.concat(relevant_frames.map { |frame| " #{frame}" })
205
+ end
206
+
207
+ parts.join("\n").truncate(5000)
208
+ end
209
+
186
210
  # Determines the status based on error type
187
211
  #
188
212
  # @param error [Exception] The exception that occurred
@@ -206,7 +230,7 @@ module RubyLLM
206
230
  execution_id: context.execution_id
207
231
  )
208
232
  rescue => e
209
- debug("Start notification failed: #{e.message}")
233
+ debug("Start notification failed: #{e.message}", context)
210
234
  end
211
235
 
212
236
  # Emits an AS::Notification for execution completion or error
@@ -243,7 +267,7 @@ module RubyLLM
243
267
  error_message: context.error&.message
244
268
  )
245
269
  rescue => e
246
- debug("Complete notification failed: #{e.message}")
270
+ debug("Complete notification failed: #{e.message}", context)
247
271
  end
248
272
 
249
273
  # Builds data for initial running execution record
@@ -295,6 +319,9 @@ module RubyLLM
295
319
  data[:root_execution_id] = context.root_execution_id || context.parent_execution_id
296
320
  end
297
321
 
322
+ # Inject tracker request_id and tags
323
+ inject_tracker_data(context, data)
324
+
298
325
  data
299
326
  end
300
327
 
@@ -322,7 +349,7 @@ module RubyLLM
322
349
  context_meta = begin
323
350
  context.metadata.dup
324
351
  rescue => e
325
- debug("Failed to read context metadata: #{e.message}")
352
+ debug("Failed to read context metadata: #{e.message}", context)
326
353
  {}
327
354
  end
328
355
  context_meta.transform_keys!(&:to_s)
@@ -365,7 +392,7 @@ module RubyLLM
365
392
  end
366
393
 
367
394
  if context.error
368
- detail_data[:error_message] = truncate_error_message(context.error.message)
395
+ detail_data[:error_message] = build_error_message(context.error)
369
396
  end
370
397
 
371
398
  if context[:tool_calls].present?
@@ -392,7 +419,7 @@ module RubyLLM
392
419
  execution.create_detail!(detail_data)
393
420
  end
394
421
  rescue => e
395
- error("Failed to save execution details: #{e.message}")
422
+ error("Failed to save execution details: #{e.message}", context)
396
423
  end
397
424
 
398
425
  # Persists execution data to database (legacy fallback)
@@ -412,7 +439,7 @@ module RubyLLM
412
439
  create_execution_record(data)
413
440
  end
414
441
  rescue => e
415
- error("Failed to record execution: #{e.message}")
442
+ error("Failed to record execution: #{e.message}", context)
416
443
  end
417
444
 
418
445
  # Builds execution data hash for the legacy single-step persistence path.
@@ -435,7 +462,7 @@ module RubyLLM
435
462
  detail_data[:user_prompt] = context.input.to_s.presence
436
463
  detail_data[:assistant_prompt] = exec_opts[:assistant_prefill] if assistant_prompt_column_exists?
437
464
  end
438
- detail_data[:error_message] = truncate_error_message(context.error.message) if context.error
465
+ detail_data[:error_message] = build_error_message(context.error) if context.error
439
466
  detail_data[:tool_calls] = context[:tool_calls] if context[:tool_calls].present?
440
467
  detail_data[:attempts] = context[:reliability_attempts] if context[:reliability_attempts].present?
441
468
  if global_config.persist_responses && context.output.respond_to?(:content)
@@ -474,7 +501,7 @@ module RubyLLM
474
501
  params = begin
475
502
  context.agent_instance.send(:options)
476
503
  rescue => e
477
- debug("Failed to extract agent options: #{e.message}")
504
+ debug("Failed to extract agent options: #{e.message}", context)
478
505
  {}
479
506
  end
480
507
  params = params.dup
@@ -505,10 +532,38 @@ module RubyLLM
505
532
  result = context.agent_instance.metadata
506
533
  result.is_a?(Hash) ? result : {}
507
534
  rescue => e
508
- debug("Failed to retrieve agent metadata: #{e.message}")
535
+ debug("Failed to retrieve agent metadata: #{e.message}", context)
509
536
  {}
510
537
  end
511
538
 
539
+ # Injects tracker request_id and tags into execution data
540
+ #
541
+ # Reads @_track_request_id and @_track_tags from the agent instance,
542
+ # which are set by BaseAgent#initialize when a Tracker is active.
543
+ #
544
+ # @param context [Context] The execution context
545
+ # @param data [Hash] The execution data hash to modify
546
+ def inject_tracker_data(context, data)
547
+ agent = context.agent_instance
548
+ return unless agent
549
+
550
+ # Inject request_id
551
+ track_request_id = agent.instance_variable_get(:@_track_request_id)
552
+ if track_request_id && data[:request_id].blank?
553
+ data[:request_id] = track_request_id
554
+ end
555
+
556
+ # Merge tracker tags into metadata
557
+ track_tags = agent.instance_variable_get(:@_track_tags)
558
+ if track_tags.is_a?(Hash) && track_tags.any?
559
+ data[:metadata] = (data[:metadata] || {}).merge(
560
+ "tags" => track_tags.transform_keys(&:to_s)
561
+ )
562
+ end
563
+ rescue
564
+ # Never let tracker data injection break execution
565
+ end
566
+
512
567
  # Sensitive parameter keys that should be redacted
513
568
  SENSITIVE_KEYS = %w[
514
569
  password token api_key secret credential auth key
@@ -527,7 +582,7 @@ module RubyLLM
527
582
  def truncate_error_message(message)
528
583
  return "" if message.nil?
529
584
 
530
- message.to_s.truncate(1000)
585
+ message.to_s.truncate(5000)
531
586
  rescue
532
587
  message.to_s[0, 1000]
533
588
  end
@@ -554,7 +609,7 @@ module RubyLLM
554
609
 
555
610
  response_data
556
611
  rescue => e
557
- error("Failed to serialize response: #{e.message}")
612
+ error("Failed to serialize response: #{e.message}", context)
558
613
  nil
559
614
  end
560
615
 
@@ -581,7 +636,7 @@ module RubyLLM
581
636
 
582
637
  detail_data[:response] = serialize_audio_response(context.output)
583
638
  rescue => e
584
- error("Failed to persist audio response: #{e.message}")
639
+ error("Failed to persist audio response: #{e.message}", context)
585
640
  end
586
641
 
587
642
  # Serializes a SpeechResult into a hash for the response column
@@ -637,7 +692,7 @@ module RubyLLM
637
692
  cfg.track_executions
638
693
  end
639
694
  rescue => e
640
- debug("Failed to check tracking config: #{e.message}")
695
+ debug("Failed to check tracking config: #{e.message}", context)
641
696
  false
642
697
  end
643
698
 
@@ -41,11 +41,13 @@ module RubyLLM
41
41
  def call(context)
42
42
  return @app.call(context) unless reliability_enabled?
43
43
 
44
- config = reliability_config
45
- models_to_try = build_models_list(context, config)
46
- total_deadline = calculate_deadline(config)
44
+ trace(context) do
45
+ config = reliability_config
46
+ models_to_try = build_models_list(context, config)
47
+ total_deadline = calculate_deadline(config)
47
48
 
48
- execute_with_reliability(context, models_to_try, config, total_deadline)
49
+ execute_with_reliability(context, models_to_try, config, total_deadline)
50
+ end
49
51
  end
50
52
 
51
53
  private
@@ -103,7 +105,7 @@ module RubyLLM
103
105
  # Check circuit breaker for this model
104
106
  breaker = get_circuit_breaker(current_model, context)
105
107
  if breaker&.open?
106
- debug("Circuit breaker open for #{current_model}, skipping")
108
+ debug("Circuit breaker open for #{current_model}, skipping", context)
107
109
  tracker.record_short_circuit(current_model)
108
110
  next
109
111
  end
@@ -41,10 +41,12 @@ module RubyLLM
41
41
  # @param context [Context] The execution context
42
42
  # @return [Context] The context with tenant fields populated
43
43
  def call(context)
44
- resolve_tenant!(context)
45
- ensure_tenant_record!(context)
46
- apply_api_configuration!(context)
47
- @app.call(context)
44
+ trace(context) do
45
+ resolve_tenant!(context)
46
+ ensure_tenant_record!(context)
47
+ apply_api_configuration!(context)
48
+ @app.call(context)
49
+ end
48
50
  end
49
51
 
50
52
  private
@@ -53,6 +53,17 @@ module RubyLLM
53
53
  helper RubyLLM::Agents::ApplicationHelper
54
54
  before_action :authenticate_dashboard!
55
55
 
56
+ rescue_from ::ActiveRecord::StatementInvalid do |e|
57
+ if e.message.include?("ruby_llm_agents_")
58
+ render plain: "RubyLLM::Agents migrations are pending.\n\n" \
59
+ "Run: rails db:migrate\n" \
60
+ "Or: rails ruby_llm_agents:doctor",
61
+ status: :service_unavailable
62
+ else
63
+ raise
64
+ end
65
+ end
66
+
56
67
  private
57
68
 
58
69
  # Authenticates dashboard access using configured method
@@ -15,6 +15,8 @@ module RubyLLM
15
15
  # result.success? # => true
16
16
  #
17
17
  class BackgroundRemovalResult
18
+ include Trackable
19
+
18
20
  attr_reader :foreground, :mask, :source_image, :model_id, :output_format,
19
21
  :alpha_matting, :refine_edges,
20
22
  :started_at, :completed_at, :tenant_id, :remover_class,
@@ -38,7 +40,7 @@ module RubyLLM
38
40
  # @param error_message [String, nil] Error message if failed
39
41
  def initialize(foreground:, mask:, source_image:, model_id:, output_format:,
40
42
  alpha_matting:, refine_edges:, started_at:, completed_at:,
41
- tenant_id:, remover_class:, error_class: nil, error_message: nil)
43
+ tenant_id:, remover_class:, error_class: nil, error_message: nil, agent_class_name: nil)
42
44
  @foreground = foreground
43
45
  @mask = mask
44
46
  @source_image = source_image
@@ -53,6 +55,10 @@ module RubyLLM
53
55
  @error_class = error_class
54
56
  @error_message = error_message
55
57
  @execution_id = nil
58
+
59
+ # Tracking
60
+ @agent_class_name = agent_class_name
61
+ register_with_tracker
56
62
  end
57
63
 
58
64
  # Loads the associated Execution record from the database