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
|
@@ -39,45 +39,47 @@ module RubyLLM
|
|
|
39
39
|
def call(context)
|
|
40
40
|
context.started_at = Time.current
|
|
41
41
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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
|
-
|
|
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
|
|
163
|
+
error_message = build_error_message(error)
|
|
162
164
|
|
|
163
165
|
update_data = {
|
|
164
166
|
status: "error",
|
|
@@ -176,13 +178,35 @@ module RubyLLM
|
|
|
176
178
|
else
|
|
177
179
|
RubyLLM::Agents::ExecutionDetail.create!(detail_attrs.merge(execution_id: execution.id))
|
|
178
180
|
end
|
|
179
|
-
rescue
|
|
180
|
-
#
|
|
181
|
+
rescue => e
|
|
182
|
+
debug("Failed to store error detail: #{e.message}")
|
|
181
183
|
end
|
|
182
184
|
rescue => e
|
|
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
|
|
@@ -205,8 +229,8 @@ module RubyLLM
|
|
|
205
229
|
tenant_id: context.tenant_id,
|
|
206
230
|
execution_id: context.execution_id
|
|
207
231
|
)
|
|
208
|
-
rescue
|
|
209
|
-
|
|
232
|
+
rescue => e
|
|
233
|
+
debug("Start notification failed: #{e.message}", context)
|
|
210
234
|
end
|
|
211
235
|
|
|
212
236
|
# Emits an AS::Notification for execution completion or error
|
|
@@ -242,8 +266,8 @@ module RubyLLM
|
|
|
242
266
|
error_class: context.error&.class&.name,
|
|
243
267
|
error_message: context.error&.message
|
|
244
268
|
)
|
|
245
|
-
rescue
|
|
246
|
-
|
|
269
|
+
rescue => e
|
|
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
|
|
|
@@ -321,7 +348,8 @@ module RubyLLM
|
|
|
321
348
|
|
|
322
349
|
context_meta = begin
|
|
323
350
|
context.metadata.dup
|
|
324
|
-
rescue
|
|
351
|
+
rescue => e
|
|
352
|
+
debug("Failed to read context metadata: #{e.message}", context)
|
|
325
353
|
{}
|
|
326
354
|
end
|
|
327
355
|
context_meta.transform_keys!(&:to_s)
|
|
@@ -364,7 +392,7 @@ module RubyLLM
|
|
|
364
392
|
end
|
|
365
393
|
|
|
366
394
|
if context.error
|
|
367
|
-
detail_data[:error_message] =
|
|
395
|
+
detail_data[:error_message] = build_error_message(context.error)
|
|
368
396
|
end
|
|
369
397
|
|
|
370
398
|
if context[:tool_calls].present?
|
|
@@ -391,7 +419,7 @@ module RubyLLM
|
|
|
391
419
|
execution.create_detail!(detail_data)
|
|
392
420
|
end
|
|
393
421
|
rescue => e
|
|
394
|
-
error("Failed to save execution details: #{e.message}")
|
|
422
|
+
error("Failed to save execution details: #{e.message}", context)
|
|
395
423
|
end
|
|
396
424
|
|
|
397
425
|
# Persists execution data to database (legacy fallback)
|
|
@@ -411,75 +439,22 @@ module RubyLLM
|
|
|
411
439
|
create_execution_record(data)
|
|
412
440
|
end
|
|
413
441
|
rescue => e
|
|
414
|
-
error("Failed to record execution: #{e.message}")
|
|
442
|
+
error("Failed to record execution: #{e.message}", context)
|
|
415
443
|
end
|
|
416
444
|
|
|
417
|
-
# Builds execution data hash
|
|
445
|
+
# Builds execution data hash for the legacy single-step persistence path.
|
|
446
|
+
#
|
|
447
|
+
# Composes from build_running_execution_data and build_completion_data
|
|
448
|
+
# to avoid duplication.
|
|
418
449
|
#
|
|
419
450
|
# @param context [Context] The execution context
|
|
420
451
|
# @param status [String] "success" or "error"
|
|
421
|
-
# @return [Hash] Execution data
|
|
452
|
+
# @return [Hash] Execution data with _detail_data for detail record
|
|
422
453
|
def build_execution_data(context, status)
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
merged_metadata = agent_meta.transform_keys(&:to_s)
|
|
426
|
-
|
|
427
|
-
context_meta = begin
|
|
428
|
-
context.metadata.dup
|
|
429
|
-
rescue
|
|
430
|
-
{}
|
|
431
|
-
end
|
|
432
|
-
context_meta.transform_keys!(&:to_s)
|
|
433
|
-
merged_metadata.merge!(context_meta)
|
|
434
|
-
|
|
435
|
-
if context.cached? && context[:cache_key]
|
|
436
|
-
merged_metadata["response_cache_key"] = context[:cache_key]
|
|
437
|
-
end
|
|
438
|
-
|
|
439
|
-
data = {
|
|
440
|
-
agent_type: context.agent_class&.name,
|
|
441
|
-
model_id: context.model,
|
|
442
|
-
status: determine_status(context, status),
|
|
443
|
-
duration_ms: context.duration_ms,
|
|
444
|
-
started_at: context.started_at,
|
|
445
|
-
completed_at: context.completed_at,
|
|
446
|
-
cache_hit: context.cached?,
|
|
447
|
-
input_tokens: context.input_tokens || 0,
|
|
448
|
-
output_tokens: context.output_tokens || 0,
|
|
449
|
-
total_cost: context.total_cost || 0,
|
|
450
|
-
attempts_count: context.attempts_made,
|
|
451
|
-
metadata: merged_metadata
|
|
452
|
-
}
|
|
453
|
-
|
|
454
|
-
# Extract tracing fields from agent metadata to dedicated columns
|
|
455
|
-
if agent_meta.any?
|
|
456
|
-
data[:trace_id] = agent_meta[:trace_id] if agent_meta[:trace_id]
|
|
457
|
-
data[:request_id] = agent_meta[:request_id] if agent_meta[:request_id]
|
|
458
|
-
data[:parent_execution_id] = agent_meta[:parent_execution_id] if agent_meta[:parent_execution_id]
|
|
459
|
-
data[:root_execution_id] = agent_meta[:root_execution_id] if agent_meta[:root_execution_id]
|
|
460
|
-
end
|
|
461
|
-
|
|
462
|
-
# Add tenant_id only if multi-tenancy is enabled and tenant is set
|
|
463
|
-
if global_config.multi_tenancy_enabled? && context.tenant_id.present?
|
|
464
|
-
data[:tenant_id] = context.tenant_id
|
|
465
|
-
end
|
|
466
|
-
|
|
467
|
-
# Error class on execution
|
|
468
|
-
if context.error
|
|
469
|
-
data[:error_class] = context.error.class.name
|
|
470
|
-
end
|
|
471
|
-
|
|
472
|
-
# Tool calls count on execution
|
|
473
|
-
if context[:tool_calls].present?
|
|
474
|
-
data[:tool_calls_count] = context[:tool_calls].size
|
|
475
|
-
end
|
|
476
|
-
|
|
477
|
-
# Attempts count on execution
|
|
478
|
-
if context[:reliability_attempts].present?
|
|
479
|
-
data[:attempts_count] = context[:reliability_attempts].size
|
|
480
|
-
end
|
|
454
|
+
data = build_running_execution_data(context)
|
|
455
|
+
.merge(build_completion_data(context, determine_status(context, status)))
|
|
481
456
|
|
|
482
|
-
#
|
|
457
|
+
# Build detail data for separate creation
|
|
483
458
|
detail_data = {parameters: sanitize_parameters(context)}
|
|
484
459
|
if global_config.persist_prompts
|
|
485
460
|
exec_opts = context.options[:options] || {}
|
|
@@ -487,18 +462,16 @@ module RubyLLM
|
|
|
487
462
|
detail_data[:user_prompt] = context.input.to_s.presence
|
|
488
463
|
detail_data[:assistant_prompt] = exec_opts[:assistant_prefill] if assistant_prompt_column_exists?
|
|
489
464
|
end
|
|
490
|
-
detail_data[:error_message] =
|
|
465
|
+
detail_data[:error_message] = build_error_message(context.error) if context.error
|
|
491
466
|
detail_data[:tool_calls] = context[:tool_calls] if context[:tool_calls].present?
|
|
492
467
|
detail_data[:attempts] = context[:reliability_attempts] if context[:reliability_attempts].present?
|
|
493
468
|
if global_config.persist_responses && context.output.respond_to?(:content)
|
|
494
469
|
detail_data[:response] = serialize_response(context)
|
|
495
470
|
end
|
|
496
471
|
|
|
497
|
-
# Persist audio data for Speaker executions
|
|
498
472
|
maybe_persist_audio_response(context, detail_data)
|
|
499
473
|
|
|
500
474
|
data[:_detail_data] = detail_data
|
|
501
|
-
|
|
502
475
|
data
|
|
503
476
|
end
|
|
504
477
|
|
|
@@ -527,7 +500,8 @@ module RubyLLM
|
|
|
527
500
|
|
|
528
501
|
params = begin
|
|
529
502
|
context.agent_instance.send(:options)
|
|
530
|
-
rescue
|
|
503
|
+
rescue => e
|
|
504
|
+
debug("Failed to extract agent options: #{e.message}", context)
|
|
531
505
|
{}
|
|
532
506
|
end
|
|
533
507
|
params = params.dup
|
|
@@ -558,10 +532,38 @@ module RubyLLM
|
|
|
558
532
|
result = context.agent_instance.metadata
|
|
559
533
|
result.is_a?(Hash) ? result : {}
|
|
560
534
|
rescue => e
|
|
561
|
-
debug("Failed to retrieve agent metadata: #{e.message}")
|
|
535
|
+
debug("Failed to retrieve agent metadata: #{e.message}", context)
|
|
562
536
|
{}
|
|
563
537
|
end
|
|
564
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
|
+
|
|
565
567
|
# Sensitive parameter keys that should be redacted
|
|
566
568
|
SENSITIVE_KEYS = %w[
|
|
567
569
|
password token api_key secret credential auth key
|
|
@@ -580,7 +582,7 @@ module RubyLLM
|
|
|
580
582
|
def truncate_error_message(message)
|
|
581
583
|
return "" if message.nil?
|
|
582
584
|
|
|
583
|
-
message.to_s.truncate(
|
|
585
|
+
message.to_s.truncate(5000)
|
|
584
586
|
rescue
|
|
585
587
|
message.to_s[0, 1000]
|
|
586
588
|
end
|
|
@@ -607,7 +609,7 @@ module RubyLLM
|
|
|
607
609
|
|
|
608
610
|
response_data
|
|
609
611
|
rescue => e
|
|
610
|
-
error("Failed to serialize response: #{e.message}")
|
|
612
|
+
error("Failed to serialize response: #{e.message}", context)
|
|
611
613
|
nil
|
|
612
614
|
end
|
|
613
615
|
|
|
@@ -634,7 +636,7 @@ module RubyLLM
|
|
|
634
636
|
|
|
635
637
|
detail_data[:response] = serialize_audio_response(context.output)
|
|
636
638
|
rescue => e
|
|
637
|
-
error("Failed to persist audio response: #{e.message}")
|
|
639
|
+
error("Failed to persist audio response: #{e.message}", context)
|
|
638
640
|
end
|
|
639
641
|
|
|
640
642
|
# Serializes a SpeechResult into a hash for the response column
|
|
@@ -689,7 +691,8 @@ module RubyLLM
|
|
|
689
691
|
else
|
|
690
692
|
cfg.track_executions
|
|
691
693
|
end
|
|
692
|
-
rescue
|
|
694
|
+
rescue => e
|
|
695
|
+
debug("Failed to check tracking config: #{e.message}", context)
|
|
693
696
|
false
|
|
694
697
|
end
|
|
695
698
|
|
|
@@ -698,7 +701,8 @@ module RubyLLM
|
|
|
698
701
|
# @return [Boolean]
|
|
699
702
|
def track_cache_hits?
|
|
700
703
|
global_config.respond_to?(:track_cache_hits) && global_config.track_cache_hits
|
|
701
|
-
rescue
|
|
704
|
+
rescue => e
|
|
705
|
+
debug("Failed to check track_cache_hits config: #{e.message}")
|
|
702
706
|
false
|
|
703
707
|
end
|
|
704
708
|
|
|
@@ -707,7 +711,8 @@ module RubyLLM
|
|
|
707
711
|
# @return [Boolean]
|
|
708
712
|
def async_logging?
|
|
709
713
|
global_config.async_logging && defined?(Infrastructure::ExecutionLoggerJob)
|
|
710
|
-
rescue
|
|
714
|
+
rescue => e
|
|
715
|
+
debug("Failed to check async_logging config: #{e.message}")
|
|
711
716
|
false
|
|
712
717
|
end
|
|
713
718
|
|
|
@@ -722,7 +727,8 @@ module RubyLLM
|
|
|
722
727
|
@_assistant_prompt_column_exists = begin
|
|
723
728
|
defined?(RubyLLM::Agents::ExecutionDetail) &&
|
|
724
729
|
RubyLLM::Agents::ExecutionDetail.column_names.include?("assistant_prompt")
|
|
725
|
-
rescue
|
|
730
|
+
rescue => e
|
|
731
|
+
debug("Failed to check assistant_prompt column: #{e.message}")
|
|
726
732
|
false
|
|
727
733
|
end
|
|
728
734
|
end
|
|
@@ -41,11 +41,13 @@ module RubyLLM
|
|
|
41
41
|
def call(context)
|
|
42
42
|
return @app.call(context) unless reliability_enabled?
|
|
43
43
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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
|
-
|
|
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
|
|
@@ -237,7 +239,7 @@ module RubyLLM
|
|
|
237
239
|
return false if attempt_index >= max_retries
|
|
238
240
|
return false if total_deadline && Time.current > total_deadline
|
|
239
241
|
# Don't retry if fallback models are available — move to next model instead
|
|
240
|
-
return false if
|
|
242
|
+
return false if fallback_models?(config)
|
|
241
243
|
|
|
242
244
|
retryable_error?(error, config)
|
|
243
245
|
end
|
|
@@ -256,7 +258,7 @@ module RubyLLM
|
|
|
256
258
|
#
|
|
257
259
|
# @param config [Hash] The reliability configuration
|
|
258
260
|
# @return [Boolean]
|
|
259
|
-
def
|
|
261
|
+
def fallback_models?(config)
|
|
260
262
|
fallbacks = config[:fallback_models]
|
|
261
263
|
fallbacks.is_a?(Array) && fallbacks.any?
|
|
262
264
|
end
|
|
@@ -317,8 +319,8 @@ module RubyLLM
|
|
|
317
319
|
event,
|
|
318
320
|
{agent_type: @agent_class&.name}.merge(extras)
|
|
319
321
|
)
|
|
320
|
-
rescue
|
|
321
|
-
|
|
322
|
+
rescue => e
|
|
323
|
+
debug("Reliability notification failed: #{e.message}")
|
|
322
324
|
end
|
|
323
325
|
|
|
324
326
|
# Sleeps without blocking other fibers when in async context
|
|
@@ -333,8 +335,8 @@ module RubyLLM
|
|
|
333
335
|
else
|
|
334
336
|
sleep(seconds)
|
|
335
337
|
end
|
|
336
|
-
rescue
|
|
337
|
-
|
|
338
|
+
rescue => e
|
|
339
|
+
debug("Async sleep failed, falling back to regular sleep: #{e.message}")
|
|
338
340
|
sleep(seconds)
|
|
339
341
|
end
|
|
340
342
|
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
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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
|
|
@@ -112,7 +114,7 @@ module RubyLLM
|
|
|
112
114
|
ensure_tenant_for_model!(tenant_object)
|
|
113
115
|
else
|
|
114
116
|
# For hash-based or string tenants, ensure a minimal record exists
|
|
115
|
-
|
|
117
|
+
find_or_create_tenant!(context.tenant_id)
|
|
116
118
|
end
|
|
117
119
|
rescue => e
|
|
118
120
|
# Don't fail the execution if tenant record creation fails
|
|
@@ -145,6 +147,18 @@ module RubyLLM
|
|
|
145
147
|
enforcement: options[:enforcement]&.to_s || "soft",
|
|
146
148
|
inherit_global_defaults: options.fetch(:inherit_global, true)
|
|
147
149
|
)
|
|
150
|
+
rescue ActiveRecord::RecordNotUnique
|
|
151
|
+
# Race condition: another thread created the record — safe to ignore
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
# Finds or creates a tenant record, handling race conditions
|
|
155
|
+
#
|
|
156
|
+
# @param tenant_id [String] The tenant identifier
|
|
157
|
+
def find_or_create_tenant!(tenant_id)
|
|
158
|
+
RubyLLM::Agents::Tenant.find_or_create_by!(tenant_id: tenant_id)
|
|
159
|
+
rescue ActiveRecord::RecordNotUnique
|
|
160
|
+
# Another thread/process created the record — just find it
|
|
161
|
+
RubyLLM::Agents::Tenant.find_by!(tenant_id: tenant_id)
|
|
148
162
|
end
|
|
149
163
|
|
|
150
164
|
# Checks if the tenants table exists (memoized)
|
|
@@ -154,7 +168,8 @@ module RubyLLM
|
|
|
154
168
|
return @tenant_table_exists if defined?(@tenant_table_exists)
|
|
155
169
|
|
|
156
170
|
@tenant_table_exists = ::ActiveRecord::Base.connection.table_exists?(:ruby_llm_agents_tenants)
|
|
157
|
-
rescue
|
|
171
|
+
rescue => e
|
|
172
|
+
debug("Failed to check tenant table existence: #{e.message}")
|
|
158
173
|
@tenant_table_exists = false
|
|
159
174
|
end
|
|
160
175
|
|
|
@@ -178,7 +193,11 @@ module RubyLLM
|
|
|
178
193
|
apply_tenant_object_api_keys!(context)
|
|
179
194
|
end
|
|
180
195
|
|
|
181
|
-
#
|
|
196
|
+
# Stores tenant API keys on the context for thread-safe per-request use.
|
|
197
|
+
#
|
|
198
|
+
# Instead of mutating the global RubyLLM configuration (which is not
|
|
199
|
+
# thread-safe), keys are stored on the context. The Pipeline::Context#llm
|
|
200
|
+
# method creates a scoped RubyLLM::Context with these keys when needed.
|
|
182
201
|
#
|
|
183
202
|
# @param context [Context] The execution context
|
|
184
203
|
def apply_tenant_object_api_keys!(context)
|
|
@@ -188,34 +207,12 @@ module RubyLLM
|
|
|
188
207
|
api_keys = tenant_object.llm_api_keys
|
|
189
208
|
return if api_keys.blank?
|
|
190
209
|
|
|
191
|
-
|
|
210
|
+
context[:tenant_api_keys] = api_keys
|
|
192
211
|
rescue => e
|
|
193
212
|
# Log but don't fail if API key extraction fails
|
|
194
213
|
warn_api_key_error("tenant object", e)
|
|
195
214
|
end
|
|
196
215
|
|
|
197
|
-
# Applies a hash of API keys to RubyLLM configuration
|
|
198
|
-
#
|
|
199
|
-
# @param api_keys [Hash] Hash of provider => key mappings
|
|
200
|
-
def apply_api_keys_to_ruby_llm(api_keys)
|
|
201
|
-
RubyLLM.configure do |config|
|
|
202
|
-
api_keys.each do |provider, key|
|
|
203
|
-
next if key.blank?
|
|
204
|
-
|
|
205
|
-
setter = api_key_setter_for(provider)
|
|
206
|
-
config.public_send(setter, key) if config.respond_to?(setter)
|
|
207
|
-
end
|
|
208
|
-
end
|
|
209
|
-
end
|
|
210
|
-
|
|
211
|
-
# Returns the setter method name for a provider's API key
|
|
212
|
-
#
|
|
213
|
-
# @param provider [Symbol, String] Provider name (e.g., :openai, :anthropic)
|
|
214
|
-
# @return [String] Setter method name (e.g., "openai_api_key=")
|
|
215
|
-
def api_key_setter_for(provider)
|
|
216
|
-
"#{provider}_api_key="
|
|
217
|
-
end
|
|
218
|
-
|
|
219
216
|
# Logs a warning about API key resolution failure
|
|
220
217
|
#
|
|
221
218
|
# @param source [String] Source that failed
|
|
@@ -236,7 +233,8 @@ module RubyLLM
|
|
|
236
233
|
return nil unless tenant.respond_to?(:llm_config)
|
|
237
234
|
|
|
238
235
|
tenant.llm_config
|
|
239
|
-
rescue
|
|
236
|
+
rescue => e
|
|
237
|
+
debug("Failed to extract tenant config: #{e.message}")
|
|
240
238
|
nil
|
|
241
239
|
end
|
|
242
240
|
end
|