ruby_llm-agents 0.4.0 → 1.0.0.beta.1
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 +225 -34
- data/app/controllers/ruby_llm/agents/agents_controller.rb +136 -16
- data/app/controllers/ruby_llm/agents/api_configurations_controller.rb +214 -0
- data/app/controllers/ruby_llm/agents/dashboard_controller.rb +29 -9
- data/app/controllers/ruby_llm/agents/{settings_controller.rb → system_config_controller.rb} +3 -3
- data/app/controllers/ruby_llm/agents/tenants_controller.rb +109 -0
- data/app/controllers/ruby_llm/agents/workflows_controller.rb +355 -0
- data/app/helpers/ruby_llm/agents/application_helper.rb +25 -0
- data/app/models/ruby_llm/agents/api_configuration.rb +386 -0
- data/app/models/ruby_llm/agents/execution.rb +3 -0
- data/app/models/ruby_llm/agents/tenant_budget.rb +112 -14
- data/app/services/ruby_llm/agents/agent_registry.rb +51 -12
- data/app/views/layouts/ruby_llm/agents/application.html.erb +5 -30
- data/app/views/ruby_llm/agents/agents/_agent.html.erb +13 -1
- data/app/views/ruby_llm/agents/agents/_config_agent.html.erb +235 -0
- data/app/views/ruby_llm/agents/agents/_config_embedder.html.erb +70 -0
- data/app/views/ruby_llm/agents/agents/_config_image_generator.html.erb +152 -0
- data/app/views/ruby_llm/agents/agents/_config_moderator.html.erb +63 -0
- data/app/views/ruby_llm/agents/agents/_config_speaker.html.erb +108 -0
- data/app/views/ruby_llm/agents/agents/_config_transcriber.html.erb +91 -0
- data/app/views/ruby_llm/agents/agents/_workflow.html.erb +1 -1
- data/app/views/ruby_llm/agents/agents/index.html.erb +74 -9
- data/app/views/ruby_llm/agents/agents/show.html.erb +18 -378
- data/app/views/ruby_llm/agents/api_configurations/_api_key_field.html.erb +34 -0
- data/app/views/ruby_llm/agents/api_configurations/_form.html.erb +288 -0
- data/app/views/ruby_llm/agents/api_configurations/edit.html.erb +95 -0
- data/app/views/ruby_llm/agents/api_configurations/edit_tenant.html.erb +97 -0
- data/app/views/ruby_llm/agents/api_configurations/show.html.erb +211 -0
- data/app/views/ruby_llm/agents/api_configurations/tenant.html.erb +179 -0
- data/app/views/ruby_llm/agents/dashboard/_action_center.html.erb +1 -1
- data/app/views/ruby_llm/agents/dashboard/_agent_comparison.html.erb +269 -15
- data/app/views/ruby_llm/agents/executions/show.html.erb +98 -0
- data/app/views/ruby_llm/agents/shared/_agent_type_badge.html.erb +93 -0
- data/app/views/ruby_llm/agents/{settings → system_config}/show.html.erb +1 -1
- data/app/views/ruby_llm/agents/tenants/_form.html.erb +150 -0
- data/app/views/ruby_llm/agents/tenants/edit.html.erb +13 -0
- data/app/views/ruby_llm/agents/tenants/index.html.erb +129 -0
- data/app/views/ruby_llm/agents/tenants/show.html.erb +374 -0
- data/app/views/ruby_llm/agents/workflows/_step_performance.html.erb +236 -0
- data/app/views/ruby_llm/agents/workflows/_structure_parallel.html.erb +76 -0
- data/app/views/ruby_llm/agents/workflows/_structure_pipeline.html.erb +74 -0
- data/app/views/ruby_llm/agents/workflows/_structure_router.html.erb +108 -0
- data/app/views/ruby_llm/agents/workflows/show.html.erb +442 -0
- data/config/routes.rb +13 -1
- data/lib/generators/ruby_llm_agents/agent_generator.rb +56 -7
- data/lib/generators/ruby_llm_agents/api_configuration_generator.rb +100 -0
- data/lib/generators/ruby_llm_agents/background_remover_generator.rb +110 -0
- data/lib/generators/ruby_llm_agents/embedder_generator.rb +107 -0
- data/lib/generators/ruby_llm_agents/image_analyzer_generator.rb +115 -0
- data/lib/generators/ruby_llm_agents/image_editor_generator.rb +108 -0
- data/lib/generators/ruby_llm_agents/image_generator_generator.rb +116 -0
- data/lib/generators/ruby_llm_agents/image_pipeline_generator.rb +178 -0
- data/lib/generators/ruby_llm_agents/image_transformer_generator.rb +109 -0
- data/lib/generators/ruby_llm_agents/image_upscaler_generator.rb +103 -0
- data/lib/generators/ruby_llm_agents/image_variator_generator.rb +102 -0
- data/lib/generators/ruby_llm_agents/install_generator.rb +76 -4
- data/lib/generators/ruby_llm_agents/restructure_generator.rb +292 -0
- data/lib/generators/ruby_llm_agents/speaker_generator.rb +121 -0
- data/lib/generators/ruby_llm_agents/templates/add_execution_type_migration.rb.tt +8 -0
- data/lib/generators/ruby_llm_agents/templates/agent.rb.tt +99 -84
- data/lib/generators/ruby_llm_agents/templates/application_agent.rb.tt +42 -40
- data/lib/generators/ruby_llm_agents/templates/application_background_remover.rb.tt +26 -0
- data/lib/generators/ruby_llm_agents/templates/application_embedder.rb.tt +50 -0
- data/lib/generators/ruby_llm_agents/templates/application_image_analyzer.rb.tt +26 -0
- data/lib/generators/ruby_llm_agents/templates/application_image_editor.rb.tt +20 -0
- data/lib/generators/ruby_llm_agents/templates/application_image_generator.rb.tt +38 -0
- data/lib/generators/ruby_llm_agents/templates/application_image_pipeline.rb.tt +139 -0
- data/lib/generators/ruby_llm_agents/templates/application_image_transformer.rb.tt +21 -0
- data/lib/generators/ruby_llm_agents/templates/application_image_upscaler.rb.tt +20 -0
- data/lib/generators/ruby_llm_agents/templates/application_image_variator.rb.tt +20 -0
- data/lib/generators/ruby_llm_agents/templates/application_speaker.rb.tt +49 -0
- data/lib/generators/ruby_llm_agents/templates/application_transcriber.rb.tt +53 -0
- data/lib/generators/ruby_llm_agents/templates/background_remover.rb.tt +44 -0
- data/lib/generators/ruby_llm_agents/templates/create_api_configurations_migration.rb.tt +90 -0
- data/lib/generators/ruby_llm_agents/templates/embedder.rb.tt +41 -0
- data/lib/generators/ruby_llm_agents/templates/image_analyzer.rb.tt +45 -0
- data/lib/generators/ruby_llm_agents/templates/image_editor.rb.tt +35 -0
- data/lib/generators/ruby_llm_agents/templates/image_generator.rb.tt +47 -0
- data/lib/generators/ruby_llm_agents/templates/image_pipeline.rb.tt +50 -0
- data/lib/generators/ruby_llm_agents/templates/image_transformer.rb.tt +44 -0
- data/lib/generators/ruby_llm_agents/templates/image_upscaler.rb.tt +38 -0
- data/lib/generators/ruby_llm_agents/templates/image_variator.rb.tt +33 -0
- data/lib/generators/ruby_llm_agents/templates/skills/AGENTS.md.tt +228 -0
- data/lib/generators/ruby_llm_agents/templates/skills/BACKGROUND_REMOVERS.md.tt +131 -0
- data/lib/generators/ruby_llm_agents/templates/skills/EMBEDDERS.md.tt +255 -0
- data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_ANALYZERS.md.tt +120 -0
- data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_EDITORS.md.tt +102 -0
- data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_GENERATORS.md.tt +282 -0
- data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_PIPELINES.md.tt +228 -0
- data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_TRANSFORMERS.md.tt +120 -0
- data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_UPSCALERS.md.tt +110 -0
- data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_VARIATORS.md.tt +120 -0
- data/lib/generators/ruby_llm_agents/templates/skills/SPEAKERS.md.tt +212 -0
- data/lib/generators/ruby_llm_agents/templates/skills/TOOLS.md.tt +227 -0
- data/lib/generators/ruby_llm_agents/templates/skills/TRANSCRIBERS.md.tt +251 -0
- data/lib/generators/ruby_llm_agents/templates/skills/WORKFLOWS.md.tt +300 -0
- data/lib/generators/ruby_llm_agents/templates/speaker.rb.tt +56 -0
- data/lib/generators/ruby_llm_agents/templates/transcriber.rb.tt +51 -0
- data/lib/generators/ruby_llm_agents/transcriber_generator.rb +107 -0
- data/lib/generators/ruby_llm_agents/upgrade_generator.rb +152 -1
- data/lib/ruby_llm/agents/audio/speaker.rb +553 -0
- data/lib/ruby_llm/agents/audio/transcriber.rb +669 -0
- data/lib/ruby_llm/agents/base_agent.rb +675 -0
- data/lib/ruby_llm/agents/core/base/moderation_dsl.rb +181 -0
- data/lib/ruby_llm/agents/core/base/moderation_execution.rb +274 -0
- data/lib/ruby_llm/agents/core/base.rb +135 -0
- data/lib/ruby_llm/agents/core/configuration.rb +981 -0
- data/lib/ruby_llm/agents/core/errors.rb +150 -0
- data/lib/ruby_llm/agents/{instrumentation.rb → core/instrumentation.rb} +93 -4
- data/lib/ruby_llm/agents/core/llm_tenant.rb +358 -0
- data/lib/ruby_llm/agents/core/resolved_config.rb +348 -0
- data/lib/ruby_llm/agents/{version.rb → core/version.rb} +1 -1
- data/lib/ruby_llm/agents/dsl/base.rb +110 -0
- data/lib/ruby_llm/agents/dsl/caching.rb +142 -0
- data/lib/ruby_llm/agents/dsl/reliability.rb +307 -0
- data/lib/ruby_llm/agents/dsl.rb +41 -0
- data/lib/ruby_llm/agents/image/analyzer/dsl.rb +130 -0
- data/lib/ruby_llm/agents/image/analyzer/execution.rb +402 -0
- data/lib/ruby_llm/agents/image/analyzer.rb +90 -0
- data/lib/ruby_llm/agents/image/background_remover/dsl.rb +154 -0
- data/lib/ruby_llm/agents/image/background_remover/execution.rb +240 -0
- data/lib/ruby_llm/agents/image/background_remover.rb +89 -0
- data/lib/ruby_llm/agents/image/concerns/image_operation_dsl.rb +91 -0
- data/lib/ruby_llm/agents/image/concerns/image_operation_execution.rb +165 -0
- data/lib/ruby_llm/agents/image/editor/dsl.rb +56 -0
- data/lib/ruby_llm/agents/image/editor/execution.rb +207 -0
- data/lib/ruby_llm/agents/image/editor.rb +92 -0
- data/lib/ruby_llm/agents/image/generator/active_storage_support.rb +127 -0
- data/lib/ruby_llm/agents/image/generator/content_policy.rb +95 -0
- data/lib/ruby_llm/agents/image/generator/pricing.rb +353 -0
- data/lib/ruby_llm/agents/image/generator/templates.rb +124 -0
- data/lib/ruby_llm/agents/image/generator.rb +455 -0
- data/lib/ruby_llm/agents/image/pipeline/dsl.rb +213 -0
- data/lib/ruby_llm/agents/image/pipeline/execution.rb +382 -0
- data/lib/ruby_llm/agents/image/pipeline.rb +97 -0
- data/lib/ruby_llm/agents/image/transformer/dsl.rb +148 -0
- data/lib/ruby_llm/agents/image/transformer/execution.rb +223 -0
- data/lib/ruby_llm/agents/image/transformer.rb +95 -0
- data/lib/ruby_llm/agents/image/upscaler/dsl.rb +83 -0
- data/lib/ruby_llm/agents/image/upscaler/execution.rb +219 -0
- data/lib/ruby_llm/agents/image/upscaler.rb +81 -0
- data/lib/ruby_llm/agents/image/variator/dsl.rb +62 -0
- data/lib/ruby_llm/agents/image/variator/execution.rb +189 -0
- data/lib/ruby_llm/agents/image/variator.rb +80 -0
- data/lib/ruby_llm/agents/{alert_manager.rb → infrastructure/alert_manager.rb} +17 -22
- data/lib/ruby_llm/agents/infrastructure/budget/budget_query.rb +145 -0
- data/lib/ruby_llm/agents/infrastructure/budget/config_resolver.rb +149 -0
- data/lib/ruby_llm/agents/infrastructure/budget/forecaster.rb +68 -0
- data/lib/ruby_llm/agents/infrastructure/budget/spend_recorder.rb +279 -0
- data/lib/ruby_llm/agents/infrastructure/budget_tracker.rb +275 -0
- data/lib/ruby_llm/agents/{execution_logger_job.rb → infrastructure/execution_logger_job.rb} +17 -1
- data/lib/ruby_llm/agents/{reliability → infrastructure/reliability}/executor.rb +2 -1
- data/lib/ruby_llm/agents/{reliability → infrastructure/reliability}/retry_strategy.rb +9 -3
- data/lib/ruby_llm/agents/{reliability.rb → infrastructure/reliability.rb} +11 -21
- data/lib/ruby_llm/agents/pipeline/builder.rb +215 -0
- data/lib/ruby_llm/agents/pipeline/context.rb +255 -0
- data/lib/ruby_llm/agents/pipeline/executor.rb +86 -0
- data/lib/ruby_llm/agents/pipeline/middleware/base.rb +124 -0
- data/lib/ruby_llm/agents/pipeline/middleware/budget.rb +95 -0
- data/lib/ruby_llm/agents/pipeline/middleware/cache.rb +171 -0
- data/lib/ruby_llm/agents/pipeline/middleware/instrumentation.rb +415 -0
- data/lib/ruby_llm/agents/pipeline/middleware/reliability.rb +276 -0
- data/lib/ruby_llm/agents/pipeline/middleware/tenant.rb +196 -0
- data/lib/ruby_llm/agents/pipeline.rb +68 -0
- data/lib/ruby_llm/agents/{engine.rb → rails/engine.rb} +79 -10
- data/lib/ruby_llm/agents/results/background_removal_result.rb +286 -0
- data/lib/ruby_llm/agents/{result.rb → results/base.rb} +73 -1
- data/lib/ruby_llm/agents/results/embedding_result.rb +243 -0
- data/lib/ruby_llm/agents/results/image_analysis_result.rb +314 -0
- data/lib/ruby_llm/agents/results/image_edit_result.rb +250 -0
- data/lib/ruby_llm/agents/results/image_generation_result.rb +346 -0
- data/lib/ruby_llm/agents/results/image_pipeline_result.rb +399 -0
- data/lib/ruby_llm/agents/results/image_transform_result.rb +251 -0
- data/lib/ruby_llm/agents/results/image_upscale_result.rb +255 -0
- data/lib/ruby_llm/agents/results/image_variation_result.rb +237 -0
- data/lib/ruby_llm/agents/results/moderation_result.rb +158 -0
- data/lib/ruby_llm/agents/results/speech_result.rb +338 -0
- data/lib/ruby_llm/agents/results/transcription_result.rb +408 -0
- data/lib/ruby_llm/agents/text/embedder.rb +444 -0
- data/lib/ruby_llm/agents/text/moderator.rb +237 -0
- data/lib/ruby_llm/agents/workflow/async.rb +220 -0
- data/lib/ruby_llm/agents/workflow/async_executor.rb +156 -0
- data/lib/ruby_llm/agents/{workflow.rb → workflow/orchestrator.rb} +6 -5
- data/lib/ruby_llm/agents/workflow/parallel.rb +34 -17
- data/lib/ruby_llm/agents/workflow/thread_pool.rb +185 -0
- data/lib/ruby_llm/agents.rb +86 -20
- metadata +189 -35
- data/lib/ruby_llm/agents/base/caching.rb +0 -40
- data/lib/ruby_llm/agents/base/cost_calculation.rb +0 -105
- data/lib/ruby_llm/agents/base/dsl.rb +0 -324
- data/lib/ruby_llm/agents/base/execution.rb +0 -283
- data/lib/ruby_llm/agents/base/reliability_dsl.rb +0 -82
- data/lib/ruby_llm/agents/base/reliability_execution.rb +0 -136
- data/lib/ruby_llm/agents/base/response_building.rb +0 -86
- data/lib/ruby_llm/agents/base/tool_tracking.rb +0 -57
- data/lib/ruby_llm/agents/base.rb +0 -209
- data/lib/ruby_llm/agents/budget_tracker.rb +0 -471
- data/lib/ruby_llm/agents/configuration.rb +0 -357
- /data/lib/ruby_llm/agents/{deprecations.rb → core/deprecations.rb} +0 -0
- /data/lib/ruby_llm/agents/{inflections.rb → core/inflections.rb} +0 -0
- /data/lib/ruby_llm/agents/{attempt_tracker.rb → infrastructure/attempt_tracker.rb} +0 -0
- /data/lib/ruby_llm/agents/{cache_helper.rb → infrastructure/cache_helper.rb} +0 -0
- /data/lib/ruby_llm/agents/{circuit_breaker.rb → infrastructure/circuit_breaker.rb} +0 -0
- /data/lib/ruby_llm/agents/{redactor.rb → infrastructure/redactor.rb} +0 -0
- /data/lib/ruby_llm/agents/{reliability → infrastructure/reliability}/breaker_manager.rb +0 -0
- /data/lib/ruby_llm/agents/{reliability → infrastructure/reliability}/execution_constraints.rb +0 -0
- /data/lib/ruby_llm/agents/{reliability → infrastructure/reliability}/fallback_routing.rb +0 -0
|
@@ -0,0 +1,415 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
module Agents
|
|
5
|
+
module Pipeline
|
|
6
|
+
module Middleware
|
|
7
|
+
# Times execution and records results for observability.
|
|
8
|
+
#
|
|
9
|
+
# This middleware provides:
|
|
10
|
+
# - Execution timing (start/end timestamps, duration)
|
|
11
|
+
# - Success/failure recording to database
|
|
12
|
+
# - Token usage and cost tracking
|
|
13
|
+
# - Error details on failure
|
|
14
|
+
#
|
|
15
|
+
# Recording can be async (via background job) or sync depending
|
|
16
|
+
# on configuration.
|
|
17
|
+
#
|
|
18
|
+
# Tracking is enabled/disabled per agent type via configuration:
|
|
19
|
+
# - track_executions (conversation agents)
|
|
20
|
+
# - track_embeddings
|
|
21
|
+
# - track_moderations
|
|
22
|
+
# - track_image_generations
|
|
23
|
+
# - track_audio
|
|
24
|
+
#
|
|
25
|
+
# @example Configuration
|
|
26
|
+
# RubyLLM::Agents.configure do |config|
|
|
27
|
+
# config.track_executions = true
|
|
28
|
+
# config.track_embeddings = true
|
|
29
|
+
# config.async_logging = true # Use background job
|
|
30
|
+
# end
|
|
31
|
+
#
|
|
32
|
+
class Instrumentation < Base
|
|
33
|
+
# Process instrumentation
|
|
34
|
+
#
|
|
35
|
+
# Creates a "running" execution record at the start so executions
|
|
36
|
+
# appear on the dashboard immediately, then updates it when complete.
|
|
37
|
+
#
|
|
38
|
+
# @param context [Context] The execution context
|
|
39
|
+
# @return [Context] The context with timing info
|
|
40
|
+
def call(context)
|
|
41
|
+
context.started_at = Time.current
|
|
42
|
+
|
|
43
|
+
# Create "running" record immediately (SYNC - must appear on dashboard)
|
|
44
|
+
execution = create_running_execution(context)
|
|
45
|
+
context.execution_id = execution&.id
|
|
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 StandardError
|
|
57
|
+
# Let ensure block handle via mark_execution_failed!
|
|
58
|
+
end
|
|
59
|
+
rescue StandardError => e
|
|
60
|
+
context.completed_at = Time.current
|
|
61
|
+
context.error = e
|
|
62
|
+
raised_exception = e
|
|
63
|
+
|
|
64
|
+
begin
|
|
65
|
+
complete_execution(execution, context, status: determine_error_status(e))
|
|
66
|
+
status_update_completed = true
|
|
67
|
+
rescue StandardError
|
|
68
|
+
# Let ensure block handle via mark_execution_failed!
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
raise
|
|
72
|
+
ensure
|
|
73
|
+
# Emergency fallback if update failed
|
|
74
|
+
mark_execution_failed!(execution, error: raised_exception || $!) unless status_update_completed
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
context
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
private
|
|
81
|
+
|
|
82
|
+
# Creates initial execution record with 'running' status
|
|
83
|
+
#
|
|
84
|
+
# Creates the record synchronously so it appears on the dashboard immediately.
|
|
85
|
+
# Returns nil on failure to avoid breaking the actual execution.
|
|
86
|
+
#
|
|
87
|
+
# @param context [Context] The execution context
|
|
88
|
+
# @return [Execution, nil] The created record, or nil on failure
|
|
89
|
+
def create_running_execution(context)
|
|
90
|
+
return nil unless tracking_enabled?(context)
|
|
91
|
+
return nil unless execution_model_available?
|
|
92
|
+
return nil if context.cached? && !track_cache_hits?
|
|
93
|
+
|
|
94
|
+
data = build_running_execution_data(context)
|
|
95
|
+
Execution.create!(data)
|
|
96
|
+
rescue StandardError => e
|
|
97
|
+
error("Failed to create running execution record: #{e.message}")
|
|
98
|
+
nil
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Updates execution record with completion data
|
|
102
|
+
#
|
|
103
|
+
# Updates the existing record with final status, duration, and metrics.
|
|
104
|
+
# Falls back to creating a new record if the initial record is nil.
|
|
105
|
+
# Errors are re-raised to allow the ensure block to handle them.
|
|
106
|
+
#
|
|
107
|
+
# @param execution [Execution, nil] The execution record to update
|
|
108
|
+
# @param context [Context] The execution context
|
|
109
|
+
# @param status [String] Final status ("success", "error", "timeout")
|
|
110
|
+
# @raise [StandardError] Re-raises any errors for ensure block to handle
|
|
111
|
+
def complete_execution(execution, context, status:)
|
|
112
|
+
return unless tracking_enabled?(context)
|
|
113
|
+
return if context.cached? && !track_cache_hits?
|
|
114
|
+
return unless execution_model_available?
|
|
115
|
+
|
|
116
|
+
# Fall back to legacy create if no execution record exists
|
|
117
|
+
unless execution
|
|
118
|
+
persist_execution(context, status: status)
|
|
119
|
+
return
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
update_data = build_completion_data(context, status)
|
|
123
|
+
|
|
124
|
+
if async_logging?
|
|
125
|
+
# For async updates, use a job (if update support exists)
|
|
126
|
+
# For now, update synchronously to ensure dashboard shows correct status
|
|
127
|
+
execution.update!(update_data)
|
|
128
|
+
else
|
|
129
|
+
execution.update!(update_data)
|
|
130
|
+
end
|
|
131
|
+
rescue StandardError => e
|
|
132
|
+
error("Failed to complete execution record: #{e.message}")
|
|
133
|
+
raise # Re-raise for ensure block to handle via mark_execution_failed!
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# Emergency fallback to mark execution as failed
|
|
137
|
+
#
|
|
138
|
+
# Uses update_all to bypass ActiveRecord callbacks and validations,
|
|
139
|
+
# ensuring the status is updated even if the model is in an invalid state.
|
|
140
|
+
# Only updates records that are still in 'running' status.
|
|
141
|
+
#
|
|
142
|
+
# @param execution [Execution, nil] The execution record
|
|
143
|
+
# @param error [Exception, nil] The exception that caused the failure
|
|
144
|
+
def mark_execution_failed!(execution, error: nil)
|
|
145
|
+
return unless execution&.id
|
|
146
|
+
return unless execution.status == "running"
|
|
147
|
+
|
|
148
|
+
error_message = error ? "#{error.class}: #{error.message}".truncate(1000) : "Unknown error"
|
|
149
|
+
|
|
150
|
+
update_data = {
|
|
151
|
+
status: "error",
|
|
152
|
+
completed_at: Time.current,
|
|
153
|
+
error_class: error&.class&.name || "UnknownError",
|
|
154
|
+
error_message: error_message
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
execution.class.where(id: execution.id, status: "running").update_all(update_data)
|
|
158
|
+
rescue StandardError => e
|
|
159
|
+
error("CRITICAL: Failed emergency status update for execution #{execution&.id}: #{e.message}")
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
# Determines the status based on error type
|
|
163
|
+
#
|
|
164
|
+
# @param error [Exception] The exception that occurred
|
|
165
|
+
# @return [String] The determined status ("timeout" or "error")
|
|
166
|
+
def determine_error_status(error)
|
|
167
|
+
error.is_a?(Timeout::Error) ? "timeout" : "error"
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
# Builds data for initial running execution record
|
|
171
|
+
#
|
|
172
|
+
# @param context [Context] The execution context
|
|
173
|
+
# @return [Hash] Execution data for creating running record
|
|
174
|
+
def build_running_execution_data(context)
|
|
175
|
+
data = {
|
|
176
|
+
agent_type: context.agent_class&.name,
|
|
177
|
+
agent_version: config(:version, "1.0"),
|
|
178
|
+
model_id: context.model,
|
|
179
|
+
status: "running",
|
|
180
|
+
started_at: context.started_at,
|
|
181
|
+
input_tokens: 0,
|
|
182
|
+
output_tokens: 0,
|
|
183
|
+
total_cost: 0,
|
|
184
|
+
attempts_count: context.attempts_made
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
# Add tenant_id only if multi-tenancy is enabled and tenant is set
|
|
188
|
+
if global_config.multi_tenancy_enabled? && context.tenant_id.present?
|
|
189
|
+
data[:tenant_id] = context.tenant_id
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
# Add sanitized parameters
|
|
193
|
+
data[:parameters] = sanitize_parameters(context)
|
|
194
|
+
|
|
195
|
+
data
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
# Builds data for completing an execution record
|
|
199
|
+
#
|
|
200
|
+
# @param context [Context] The execution context
|
|
201
|
+
# @param status [String] Final status ("success", "error", "timeout")
|
|
202
|
+
# @return [Hash] Update data for completing the record
|
|
203
|
+
def build_completion_data(context, status)
|
|
204
|
+
data = {
|
|
205
|
+
status: status,
|
|
206
|
+
completed_at: context.completed_at,
|
|
207
|
+
duration_ms: context.duration_ms,
|
|
208
|
+
cache_hit: context.cached?,
|
|
209
|
+
input_tokens: context.input_tokens || 0,
|
|
210
|
+
output_tokens: context.output_tokens || 0,
|
|
211
|
+
total_cost: context.total_cost || 0,
|
|
212
|
+
attempts_count: context.attempts_made
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
# Add cache key for cache hit executions
|
|
216
|
+
if context.cached? && context[:cache_key]
|
|
217
|
+
data[:response_cache_key] = context[:cache_key]
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
# Add error details if present
|
|
221
|
+
if context.error
|
|
222
|
+
data[:error_class] = context.error.class.name
|
|
223
|
+
data[:error_message] = truncate_error_message(context.error.message)
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
# Add custom metadata
|
|
227
|
+
data[:metadata] = context.metadata if context.metadata.any?
|
|
228
|
+
|
|
229
|
+
data
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
# Persists execution data to database (legacy fallback)
|
|
233
|
+
#
|
|
234
|
+
# Used when initial running record creation failed.
|
|
235
|
+
#
|
|
236
|
+
# @param context [Context] The execution context
|
|
237
|
+
# @param status [String] "success" or "error"
|
|
238
|
+
def persist_execution(context, status:)
|
|
239
|
+
return unless execution_model_available?
|
|
240
|
+
|
|
241
|
+
data = build_execution_data(context, status)
|
|
242
|
+
|
|
243
|
+
if async_logging?
|
|
244
|
+
queue_async_logging(data)
|
|
245
|
+
else
|
|
246
|
+
create_execution_record(data)
|
|
247
|
+
end
|
|
248
|
+
rescue StandardError => e
|
|
249
|
+
error("Failed to record execution: #{e.message}")
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
# Builds execution data hash
|
|
253
|
+
#
|
|
254
|
+
# @param context [Context] The execution context
|
|
255
|
+
# @param status [String] "success" or "error"
|
|
256
|
+
# @return [Hash] Execution data
|
|
257
|
+
def build_execution_data(context, status)
|
|
258
|
+
data = {
|
|
259
|
+
agent_type: context.agent_class&.name,
|
|
260
|
+
agent_version: config(:version, "1.0"),
|
|
261
|
+
model_id: context.model,
|
|
262
|
+
status: determine_status(context, status),
|
|
263
|
+
duration_ms: context.duration_ms,
|
|
264
|
+
started_at: context.started_at,
|
|
265
|
+
completed_at: context.completed_at,
|
|
266
|
+
cache_hit: context.cached?,
|
|
267
|
+
input_tokens: context.input_tokens || 0,
|
|
268
|
+
output_tokens: context.output_tokens || 0,
|
|
269
|
+
total_cost: context.total_cost || 0,
|
|
270
|
+
attempts_count: context.attempts_made
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
# Add tenant_id only if multi-tenancy is enabled and tenant is set
|
|
274
|
+
if global_config.multi_tenancy_enabled? && context.tenant_id.present?
|
|
275
|
+
data[:tenant_id] = context.tenant_id
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
# Add cache key for cache hit executions
|
|
279
|
+
if context.cached? && context[:cache_key]
|
|
280
|
+
data[:response_cache_key] = context[:cache_key]
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
# Add error details if present
|
|
284
|
+
if context.error
|
|
285
|
+
data[:error_class] = context.error.class.name
|
|
286
|
+
data[:error_message] = truncate_error_message(context.error.message)
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
# Add custom metadata
|
|
290
|
+
data[:metadata] = context.metadata if context.metadata.any?
|
|
291
|
+
|
|
292
|
+
# Add sanitized parameters
|
|
293
|
+
data[:parameters] = sanitize_parameters(context)
|
|
294
|
+
|
|
295
|
+
data
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
# Determines the status based on context and error type
|
|
299
|
+
#
|
|
300
|
+
# @param context [Context] The execution context
|
|
301
|
+
# @param base_status [String] The base status ("success" or "error")
|
|
302
|
+
# @return [String] The determined status
|
|
303
|
+
def determine_status(context, base_status)
|
|
304
|
+
return base_status if base_status == "success"
|
|
305
|
+
|
|
306
|
+
# Check for timeout errors
|
|
307
|
+
if context.error.is_a?(Timeout::Error)
|
|
308
|
+
"timeout"
|
|
309
|
+
else
|
|
310
|
+
base_status
|
|
311
|
+
end
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
# Sanitizes parameters for storage, redacting sensitive values
|
|
315
|
+
#
|
|
316
|
+
# @param context [Context] The execution context
|
|
317
|
+
# @return [Hash] Sanitized parameters
|
|
318
|
+
def sanitize_parameters(context)
|
|
319
|
+
return {} unless context.agent_instance.respond_to?(:options, true)
|
|
320
|
+
|
|
321
|
+
params = context.agent_instance.send(:options) rescue {}
|
|
322
|
+
params = params.dup
|
|
323
|
+
params.transform_keys!(&:to_s)
|
|
324
|
+
|
|
325
|
+
SENSITIVE_KEYS.each do |key|
|
|
326
|
+
params[key] = "[REDACTED]" if params.key?(key)
|
|
327
|
+
end
|
|
328
|
+
|
|
329
|
+
params
|
|
330
|
+
end
|
|
331
|
+
|
|
332
|
+
# Sensitive parameter keys that should be redacted
|
|
333
|
+
SENSITIVE_KEYS = %w[
|
|
334
|
+
password token api_key secret credential auth key
|
|
335
|
+
access_token refresh_token private_key secret_key
|
|
336
|
+
].freeze
|
|
337
|
+
|
|
338
|
+
# Truncates error message to prevent database issues
|
|
339
|
+
#
|
|
340
|
+
# @param message [String] The error message
|
|
341
|
+
# @return [String] Truncated message
|
|
342
|
+
def truncate_error_message(message)
|
|
343
|
+
return "" if message.nil?
|
|
344
|
+
|
|
345
|
+
message.to_s.truncate(1000)
|
|
346
|
+
rescue StandardError
|
|
347
|
+
message.to_s[0, 1000]
|
|
348
|
+
end
|
|
349
|
+
|
|
350
|
+
# Queues async logging via background job
|
|
351
|
+
#
|
|
352
|
+
# @param data [Hash] Execution data
|
|
353
|
+
def queue_async_logging(data)
|
|
354
|
+
Infrastructure::ExecutionLoggerJob.perform_later(data)
|
|
355
|
+
end
|
|
356
|
+
|
|
357
|
+
# Creates execution record synchronously
|
|
358
|
+
#
|
|
359
|
+
# @param data [Hash] Execution data
|
|
360
|
+
def create_execution_record(data)
|
|
361
|
+
Execution.create!(data)
|
|
362
|
+
end
|
|
363
|
+
|
|
364
|
+
# Returns whether tracking is enabled for this agent type
|
|
365
|
+
#
|
|
366
|
+
# @param context [Context] The execution context
|
|
367
|
+
# @return [Boolean]
|
|
368
|
+
def tracking_enabled?(context)
|
|
369
|
+
cfg = global_config
|
|
370
|
+
|
|
371
|
+
case context.agent_type
|
|
372
|
+
when :embedding
|
|
373
|
+
cfg.track_embeddings
|
|
374
|
+
when :moderation
|
|
375
|
+
cfg.track_moderation
|
|
376
|
+
when :image
|
|
377
|
+
cfg.track_image_generation
|
|
378
|
+
when :audio
|
|
379
|
+
cfg.track_audio
|
|
380
|
+
else
|
|
381
|
+
cfg.track_executions
|
|
382
|
+
end
|
|
383
|
+
rescue StandardError
|
|
384
|
+
false
|
|
385
|
+
end
|
|
386
|
+
|
|
387
|
+
# Returns whether to track cache hits
|
|
388
|
+
#
|
|
389
|
+
# @return [Boolean]
|
|
390
|
+
def track_cache_hits?
|
|
391
|
+
global_config.respond_to?(:track_cache_hits) && global_config.track_cache_hits
|
|
392
|
+
rescue StandardError
|
|
393
|
+
false
|
|
394
|
+
end
|
|
395
|
+
|
|
396
|
+
# Returns whether async logging is enabled
|
|
397
|
+
#
|
|
398
|
+
# @return [Boolean]
|
|
399
|
+
def async_logging?
|
|
400
|
+
global_config.async_logging && defined?(Infrastructure::ExecutionLoggerJob)
|
|
401
|
+
rescue StandardError
|
|
402
|
+
false
|
|
403
|
+
end
|
|
404
|
+
|
|
405
|
+
# Returns whether the Execution model is available
|
|
406
|
+
#
|
|
407
|
+
# @return [Boolean]
|
|
408
|
+
def execution_model_available?
|
|
409
|
+
defined?(RubyLLM::Agents::Execution)
|
|
410
|
+
end
|
|
411
|
+
end
|
|
412
|
+
end
|
|
413
|
+
end
|
|
414
|
+
end
|
|
415
|
+
end
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
module Agents
|
|
5
|
+
module Pipeline
|
|
6
|
+
module Middleware
|
|
7
|
+
# Handles retries, fallbacks, and circuit breakers.
|
|
8
|
+
#
|
|
9
|
+
# This middleware provides reliability features for agent executions:
|
|
10
|
+
# - Retries with configurable backoff (constant or exponential)
|
|
11
|
+
# - Model fallbacks when primary model fails
|
|
12
|
+
# - Circuit breaker protection per model
|
|
13
|
+
# - Total timeout across all attempts
|
|
14
|
+
#
|
|
15
|
+
# Reliability is enabled via the agent's reliability DSL:
|
|
16
|
+
# class MyAgent < ApplicationAgent
|
|
17
|
+
# reliability do
|
|
18
|
+
# retries max: 3, backoff: :exponential
|
|
19
|
+
# fallback_models "gpt-4o-mini"
|
|
20
|
+
# total_timeout 30
|
|
21
|
+
# circuit_breaker errors: 5, within: 60
|
|
22
|
+
# end
|
|
23
|
+
# end
|
|
24
|
+
#
|
|
25
|
+
# @example Simple retry
|
|
26
|
+
# class MyEmbedder < RubyLLM::Agents::Embedder
|
|
27
|
+
# model "text-embedding-3-small"
|
|
28
|
+
# reliability do
|
|
29
|
+
# retries max: 2
|
|
30
|
+
# end
|
|
31
|
+
# end
|
|
32
|
+
#
|
|
33
|
+
class Reliability < Base
|
|
34
|
+
# Process with reliability features
|
|
35
|
+
#
|
|
36
|
+
# @param context [Context] The execution context
|
|
37
|
+
# @return [Context] The context after execution
|
|
38
|
+
# @raise [AllModelsFailedError] If all models fail
|
|
39
|
+
# @raise [TotalTimeoutError] If total timeout exceeded
|
|
40
|
+
# @raise [CircuitOpenError] If circuit breaker is open for all models
|
|
41
|
+
def call(context)
|
|
42
|
+
return @app.call(context) unless reliability_enabled?
|
|
43
|
+
|
|
44
|
+
config = reliability_config
|
|
45
|
+
models_to_try = build_models_list(context, config)
|
|
46
|
+
total_deadline = calculate_deadline(config)
|
|
47
|
+
|
|
48
|
+
execute_with_reliability(context, models_to_try, config, total_deadline)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
private
|
|
52
|
+
|
|
53
|
+
# Returns whether reliability is enabled for this agent
|
|
54
|
+
#
|
|
55
|
+
# @return [Boolean]
|
|
56
|
+
def reliability_enabled?
|
|
57
|
+
@agent_class&.respond_to?(:reliability_config) &&
|
|
58
|
+
@agent_class.reliability_config.present?
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Returns the reliability configuration from the agent class
|
|
62
|
+
#
|
|
63
|
+
# @return [Hash] The reliability configuration
|
|
64
|
+
def reliability_config
|
|
65
|
+
@agent_class.reliability_config || {}
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Builds the list of models to try
|
|
69
|
+
#
|
|
70
|
+
# @param context [Context] The execution context
|
|
71
|
+
# @param config [Hash] The reliability configuration
|
|
72
|
+
# @return [Array<String>] List of models
|
|
73
|
+
def build_models_list(context, config)
|
|
74
|
+
primary = context.model || @agent_class&.model
|
|
75
|
+
fallbacks = config[:fallback_models] || []
|
|
76
|
+
[primary, *fallbacks].compact.uniq
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Calculates the total deadline for all attempts
|
|
80
|
+
#
|
|
81
|
+
# @param config [Hash] The reliability configuration
|
|
82
|
+
# @return [Time, nil] The deadline or nil if no timeout
|
|
83
|
+
def calculate_deadline(config)
|
|
84
|
+
return nil unless config[:total_timeout]
|
|
85
|
+
|
|
86
|
+
Time.current + config[:total_timeout]
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Executes with retry, fallback, and circuit breaker logic
|
|
90
|
+
#
|
|
91
|
+
# @param context [Context] The execution context
|
|
92
|
+
# @param models_to_try [Array<String>] List of models to try
|
|
93
|
+
# @param config [Hash] The reliability configuration
|
|
94
|
+
# @param total_deadline [Time, nil] The total deadline
|
|
95
|
+
# @return [Context] The context after execution
|
|
96
|
+
def execute_with_reliability(context, models_to_try, config, total_deadline)
|
|
97
|
+
started_at = Time.current
|
|
98
|
+
last_error = nil
|
|
99
|
+
context.attempts_made = 0
|
|
100
|
+
|
|
101
|
+
models_to_try.each do |current_model|
|
|
102
|
+
# Check circuit breaker for this model
|
|
103
|
+
breaker = get_circuit_breaker(current_model, context)
|
|
104
|
+
if breaker&.open?
|
|
105
|
+
debug("Circuit breaker open for #{current_model}, skipping")
|
|
106
|
+
next
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
result = try_model_with_retries(
|
|
110
|
+
context: context,
|
|
111
|
+
model: current_model,
|
|
112
|
+
config: config,
|
|
113
|
+
total_deadline: total_deadline,
|
|
114
|
+
started_at: started_at,
|
|
115
|
+
breaker: breaker
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
return result if result
|
|
119
|
+
|
|
120
|
+
# Capture the last error from context for the final error
|
|
121
|
+
last_error = context.error
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# All models exhausted
|
|
125
|
+
raise Agents::Reliability::AllModelsExhaustedError.new(models_to_try, last_error)
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# Tries a model with retry logic
|
|
129
|
+
#
|
|
130
|
+
# @param context [Context] The execution context
|
|
131
|
+
# @param model [String] The model to try
|
|
132
|
+
# @param config [Hash] The reliability configuration
|
|
133
|
+
# @param total_deadline [Time, nil] The total deadline
|
|
134
|
+
# @param started_at [Time] When execution started
|
|
135
|
+
# @param breaker [CircuitBreaker, nil] The circuit breaker for this model
|
|
136
|
+
# @return [Context, nil] The context if successful, nil to try next model
|
|
137
|
+
def try_model_with_retries(context:, model:, config:, total_deadline:, started_at:, breaker:)
|
|
138
|
+
retries_config = config[:retries] || {}
|
|
139
|
+
max_retries = retries_config[:max] || 0
|
|
140
|
+
attempt_index = 0
|
|
141
|
+
|
|
142
|
+
loop do
|
|
143
|
+
# Check total timeout
|
|
144
|
+
check_total_timeout!(total_deadline, started_at)
|
|
145
|
+
|
|
146
|
+
context.attempt = attempt_index + 1
|
|
147
|
+
context.attempts_made += 1
|
|
148
|
+
|
|
149
|
+
begin
|
|
150
|
+
# Override the model for this attempt
|
|
151
|
+
original_model = context.model
|
|
152
|
+
context.model = model
|
|
153
|
+
|
|
154
|
+
@app.call(context)
|
|
155
|
+
|
|
156
|
+
# Success - record in circuit breaker
|
|
157
|
+
breaker&.record_success!
|
|
158
|
+
|
|
159
|
+
return context
|
|
160
|
+
|
|
161
|
+
rescue StandardError => e
|
|
162
|
+
context.error = e
|
|
163
|
+
breaker&.record_failure!
|
|
164
|
+
|
|
165
|
+
# Check if we should retry
|
|
166
|
+
if should_retry?(e, config, attempt_index, max_retries, total_deadline)
|
|
167
|
+
attempt_index += 1
|
|
168
|
+
delay = calculate_backoff(retries_config, attempt_index)
|
|
169
|
+
async_aware_sleep(delay)
|
|
170
|
+
else
|
|
171
|
+
# Move to next model
|
|
172
|
+
return nil
|
|
173
|
+
end
|
|
174
|
+
ensure
|
|
175
|
+
# Restore original model if we're going to retry or try another model
|
|
176
|
+
context.model = original_model if context.error
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
# Checks if we've exceeded the total timeout
|
|
182
|
+
#
|
|
183
|
+
# @param deadline [Time, nil] The deadline
|
|
184
|
+
# @param started_at [Time] When execution started
|
|
185
|
+
# @raise [TotalTimeoutError] If timeout exceeded
|
|
186
|
+
def check_total_timeout!(deadline, started_at)
|
|
187
|
+
return unless deadline && Time.current > deadline
|
|
188
|
+
|
|
189
|
+
elapsed = Time.current - started_at
|
|
190
|
+
timeout_value = deadline - started_at + elapsed
|
|
191
|
+
raise Agents::Reliability::TotalTimeoutError.new(timeout_value, elapsed)
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
# Determines if we should retry the error
|
|
195
|
+
#
|
|
196
|
+
# @param error [Exception] The error that occurred
|
|
197
|
+
# @param config [Hash] The reliability configuration
|
|
198
|
+
# @param attempt_index [Integer] Current attempt index
|
|
199
|
+
# @param max_retries [Integer] Maximum retries allowed
|
|
200
|
+
# @param total_deadline [Time, nil] The total deadline
|
|
201
|
+
# @return [Boolean] Whether to retry
|
|
202
|
+
def should_retry?(error, config, attempt_index, max_retries, total_deadline)
|
|
203
|
+
return false if attempt_index >= max_retries
|
|
204
|
+
return false if total_deadline && Time.current > total_deadline
|
|
205
|
+
|
|
206
|
+
retryable_error?(error, config)
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
# Checks if an error is retryable
|
|
210
|
+
#
|
|
211
|
+
# @param error [Exception] The error to check
|
|
212
|
+
# @param config [Hash] The reliability configuration
|
|
213
|
+
# @return [Boolean] Whether the error is retryable
|
|
214
|
+
def retryable_error?(error, config)
|
|
215
|
+
custom_errors = config.dig(:retries, :on) || []
|
|
216
|
+
custom_patterns = config[:retryable_patterns]
|
|
217
|
+
|
|
218
|
+
Agents::Reliability.retryable_error?(
|
|
219
|
+
error,
|
|
220
|
+
custom_errors: custom_errors,
|
|
221
|
+
custom_patterns: custom_patterns
|
|
222
|
+
)
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
# Calculates the backoff delay
|
|
226
|
+
#
|
|
227
|
+
# @param retries_config [Hash] The retries configuration
|
|
228
|
+
# @param attempt_index [Integer] The current attempt index
|
|
229
|
+
# @return [Float] The delay in seconds
|
|
230
|
+
def calculate_backoff(retries_config, attempt_index)
|
|
231
|
+
Agents::Reliability.calculate_backoff(
|
|
232
|
+
strategy: retries_config[:backoff] || :exponential,
|
|
233
|
+
base: retries_config[:base] || 0.4,
|
|
234
|
+
max_delay: retries_config[:max_delay] || 3.0,
|
|
235
|
+
attempt: attempt_index
|
|
236
|
+
)
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
# Gets or creates a circuit breaker for a model
|
|
240
|
+
#
|
|
241
|
+
# @param model_id [String] The model identifier
|
|
242
|
+
# @param context [Context] The execution context
|
|
243
|
+
# @return [CircuitBreaker, nil] The circuit breaker or nil
|
|
244
|
+
def get_circuit_breaker(model_id, context)
|
|
245
|
+
cb_config = reliability_config[:circuit_breaker]
|
|
246
|
+
return nil unless cb_config
|
|
247
|
+
|
|
248
|
+
CircuitBreaker.from_config(
|
|
249
|
+
@agent_class&.name,
|
|
250
|
+
model_id,
|
|
251
|
+
cb_config,
|
|
252
|
+
tenant_id: context.tenant_id
|
|
253
|
+
)
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
# Sleeps without blocking other fibers when in async context
|
|
257
|
+
#
|
|
258
|
+
# @param seconds [Numeric] Duration to sleep
|
|
259
|
+
# @return [void]
|
|
260
|
+
def async_aware_sleep(seconds)
|
|
261
|
+
config = global_config
|
|
262
|
+
|
|
263
|
+
if config.respond_to?(:async_context?) && config.async_context?
|
|
264
|
+
::Async::Task.current.sleep(seconds)
|
|
265
|
+
else
|
|
266
|
+
sleep(seconds)
|
|
267
|
+
end
|
|
268
|
+
rescue StandardError
|
|
269
|
+
# Fall back to regular sleep if async detection fails
|
|
270
|
+
sleep(seconds)
|
|
271
|
+
end
|
|
272
|
+
end
|
|
273
|
+
end
|
|
274
|
+
end
|
|
275
|
+
end
|
|
276
|
+
end
|