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
|
@@ -15,19 +15,21 @@ module RubyLLM
|
|
|
15
15
|
#
|
|
16
16
|
# @api private
|
|
17
17
|
class RetryStrategy
|
|
18
|
-
attr_reader :max, :backoff, :base, :max_delay, :custom_errors
|
|
18
|
+
attr_reader :max, :backoff, :base, :max_delay, :custom_errors, :custom_patterns
|
|
19
19
|
|
|
20
20
|
# @param max [Integer] Maximum retry attempts
|
|
21
21
|
# @param backoff [Symbol] :constant or :exponential
|
|
22
22
|
# @param base [Float] Base delay in seconds
|
|
23
23
|
# @param max_delay [Float] Maximum delay cap
|
|
24
24
|
# @param on [Array<Class>] Additional error classes to retry on
|
|
25
|
-
|
|
25
|
+
# @param patterns [Array<String>, nil] Additional patterns to match in error messages
|
|
26
|
+
def initialize(max: 0, backoff: :exponential, base: 0.4, max_delay: 3.0, on: [], patterns: nil)
|
|
26
27
|
@max = max
|
|
27
28
|
@backoff = backoff
|
|
28
29
|
@base = base
|
|
29
30
|
@max_delay = max_delay
|
|
30
31
|
@custom_errors = Array(on)
|
|
32
|
+
@custom_patterns = patterns
|
|
31
33
|
end
|
|
32
34
|
|
|
33
35
|
# Determines if retry should occur
|
|
@@ -61,7 +63,11 @@ module RubyLLM
|
|
|
61
63
|
# @param error [Exception] The error to check
|
|
62
64
|
# @return [Boolean] true if retryable
|
|
63
65
|
def retryable?(error)
|
|
64
|
-
RubyLLM::Agents::Reliability.retryable_error?(
|
|
66
|
+
RubyLLM::Agents::Reliability.retryable_error?(
|
|
67
|
+
error,
|
|
68
|
+
custom_errors: custom_errors,
|
|
69
|
+
custom_patterns: custom_patterns
|
|
70
|
+
)
|
|
65
71
|
end
|
|
66
72
|
|
|
67
73
|
# Returns all retryable error classes
|
|
@@ -124,10 +124,12 @@ module RubyLLM
|
|
|
124
124
|
#
|
|
125
125
|
# @param error [Exception] The error to check
|
|
126
126
|
# @param custom_errors [Array<Class>] Additional error classes to consider retryable
|
|
127
|
+
# @param custom_patterns [Array<String>, nil] Additional patterns to check in error messages
|
|
127
128
|
# @return [Boolean] true if the error is retryable
|
|
128
|
-
def retryable_error?(error, custom_errors: [])
|
|
129
|
+
def retryable_error?(error, custom_errors: [], custom_patterns: nil)
|
|
129
130
|
all_retryable = default_retryable_errors + Array(custom_errors)
|
|
130
|
-
all_retryable.any? { |klass| error.is_a?(klass) } ||
|
|
131
|
+
all_retryable.any? { |klass| error.is_a?(klass) } ||
|
|
132
|
+
retryable_by_message?(error, custom_patterns: custom_patterns)
|
|
131
133
|
end
|
|
132
134
|
|
|
133
135
|
# Determines if an error is retryable based on its message content
|
|
@@ -136,32 +138,20 @@ module RubyLLM
|
|
|
136
138
|
# but can be identified by their message.
|
|
137
139
|
#
|
|
138
140
|
# @param error [Exception] The error to check
|
|
141
|
+
# @param custom_patterns [Array<String>, nil] Additional patterns to check
|
|
139
142
|
# @return [Boolean] true if the error message indicates a retryable condition
|
|
140
|
-
def retryable_by_message?(error)
|
|
143
|
+
def retryable_by_message?(error, custom_patterns: nil)
|
|
141
144
|
message = error.message.to_s.downcase
|
|
142
|
-
retryable_patterns.any? { |pattern| message.include?(pattern) }
|
|
145
|
+
retryable_patterns(custom_patterns: custom_patterns).any? { |pattern| message.include?(pattern) }
|
|
143
146
|
end
|
|
144
147
|
|
|
145
148
|
# Patterns in error messages that indicate retryable errors
|
|
146
149
|
#
|
|
150
|
+
# @param custom_patterns [Array<String>, nil] Additional patterns to include
|
|
147
151
|
# @return [Array<String>] Patterns to match against error messages
|
|
148
|
-
def retryable_patterns
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
"rate_limit",
|
|
152
|
-
"too many requests",
|
|
153
|
-
"429",
|
|
154
|
-
"500",
|
|
155
|
-
"502",
|
|
156
|
-
"503",
|
|
157
|
-
"504",
|
|
158
|
-
"service unavailable",
|
|
159
|
-
"internal server error",
|
|
160
|
-
"bad gateway",
|
|
161
|
-
"gateway timeout",
|
|
162
|
-
"overloaded",
|
|
163
|
-
"capacity"
|
|
164
|
-
].freeze
|
|
152
|
+
def retryable_patterns(custom_patterns: nil)
|
|
153
|
+
base = RubyLLM::Agents.configuration.all_retryable_patterns
|
|
154
|
+
custom_patterns ? (base + Array(custom_patterns)).uniq : base
|
|
165
155
|
end
|
|
166
156
|
|
|
167
157
|
# Calculates the backoff delay for a retry attempt
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
module Agents
|
|
5
|
+
module Pipeline
|
|
6
|
+
# Builds the middleware pipeline from agent DSL configuration.
|
|
7
|
+
#
|
|
8
|
+
# The builder allows both manual pipeline construction and automatic
|
|
9
|
+
# construction based on agent DSL settings.
|
|
10
|
+
#
|
|
11
|
+
# @example Manual pipeline construction
|
|
12
|
+
# builder = Builder.new(MyEmbedder)
|
|
13
|
+
# builder.use(Middleware::Tenant)
|
|
14
|
+
# builder.use(Middleware::Cache)
|
|
15
|
+
# builder.use(Middleware::Instrumentation)
|
|
16
|
+
# pipeline = builder.build(core_executor)
|
|
17
|
+
#
|
|
18
|
+
# @example Automatic construction from DSL
|
|
19
|
+
# pipeline = Builder.for(MyEmbedder).build(core_executor)
|
|
20
|
+
#
|
|
21
|
+
# @example With custom middleware insertion
|
|
22
|
+
# builder = Builder.for(MyEmbedder)
|
|
23
|
+
# builder.insert_before(Middleware::Instrumentation, MyLoggingMiddleware)
|
|
24
|
+
# pipeline = builder.build(core_executor)
|
|
25
|
+
#
|
|
26
|
+
class Builder
|
|
27
|
+
# @return [Class] The agent class this builder is for
|
|
28
|
+
attr_reader :agent_class
|
|
29
|
+
|
|
30
|
+
# @return [Array<Class>] The middleware stack (in execution order)
|
|
31
|
+
attr_reader :stack
|
|
32
|
+
|
|
33
|
+
# Creates a new builder for an agent class
|
|
34
|
+
#
|
|
35
|
+
# @param agent_class [Class] The agent class
|
|
36
|
+
def initialize(agent_class)
|
|
37
|
+
@agent_class = agent_class
|
|
38
|
+
@stack = []
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Add middleware to the end of the stack
|
|
42
|
+
#
|
|
43
|
+
# @param middleware_class [Class] Middleware class to add
|
|
44
|
+
# @return [self] For method chaining
|
|
45
|
+
def use(middleware_class)
|
|
46
|
+
@stack << middleware_class
|
|
47
|
+
self
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Insert middleware before another middleware
|
|
51
|
+
#
|
|
52
|
+
# @param existing [Class] The middleware to insert before
|
|
53
|
+
# @param new_middleware [Class] The middleware to insert
|
|
54
|
+
# @return [self] For method chaining
|
|
55
|
+
# @raise [ArgumentError] If existing middleware not found
|
|
56
|
+
def insert_before(existing, new_middleware)
|
|
57
|
+
index = @stack.index(existing)
|
|
58
|
+
raise ArgumentError, "#{existing} not found in stack" unless index
|
|
59
|
+
|
|
60
|
+
@stack.insert(index, new_middleware)
|
|
61
|
+
self
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Insert middleware after another middleware
|
|
65
|
+
#
|
|
66
|
+
# @param existing [Class] The middleware to insert after
|
|
67
|
+
# @param new_middleware [Class] The middleware to insert
|
|
68
|
+
# @return [self] For method chaining
|
|
69
|
+
# @raise [ArgumentError] If existing middleware not found
|
|
70
|
+
def insert_after(existing, new_middleware)
|
|
71
|
+
index = @stack.index(existing)
|
|
72
|
+
raise ArgumentError, "#{existing} not found in stack" unless index
|
|
73
|
+
|
|
74
|
+
@stack.insert(index + 1, new_middleware)
|
|
75
|
+
self
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Remove a middleware from the stack
|
|
79
|
+
#
|
|
80
|
+
# @param middleware_class [Class] The middleware to remove
|
|
81
|
+
# @return [self] For method chaining
|
|
82
|
+
def delete(middleware_class)
|
|
83
|
+
@stack.delete(middleware_class)
|
|
84
|
+
self
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Build the pipeline, wrapping the core executor
|
|
88
|
+
#
|
|
89
|
+
# Middleware is wrapped in reverse order so that the first
|
|
90
|
+
# middleware in the stack is the outermost wrapper.
|
|
91
|
+
#
|
|
92
|
+
# @param core [#call] The core execution logic (usually an Executor)
|
|
93
|
+
# @return [#call] The complete pipeline
|
|
94
|
+
def build(core)
|
|
95
|
+
@stack.reverse.reduce(core) do |app, middleware_class|
|
|
96
|
+
middleware_class.new(app, @agent_class)
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Returns whether the stack includes a middleware
|
|
101
|
+
#
|
|
102
|
+
# @param middleware_class [Class] The middleware class to check
|
|
103
|
+
# @return [Boolean]
|
|
104
|
+
def include?(middleware_class)
|
|
105
|
+
@stack.include?(middleware_class)
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Returns the stack as an array (copy)
|
|
109
|
+
#
|
|
110
|
+
# @return [Array<Class>]
|
|
111
|
+
def to_a
|
|
112
|
+
@stack.dup
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
class << self
|
|
116
|
+
# Build default pipeline for an agent class
|
|
117
|
+
#
|
|
118
|
+
# Reads DSL configuration to determine which middleware to include.
|
|
119
|
+
# The order is:
|
|
120
|
+
# 1. Tenant (always - resolves tenant context)
|
|
121
|
+
# 2. Budget (if enabled - checks budget before execution)
|
|
122
|
+
# 3. Instrumentation (always - tracks execution, including cache hits)
|
|
123
|
+
# 4. Cache (if enabled - returns cached results)
|
|
124
|
+
# 5. Reliability (if enabled - retries and fallbacks)
|
|
125
|
+
#
|
|
126
|
+
# Note: Instrumentation must come BEFORE Cache so it can track cache hits.
|
|
127
|
+
# When Cache returns early on a hit, Instrumentation still sees it.
|
|
128
|
+
#
|
|
129
|
+
# @param agent_class [Class] The agent class
|
|
130
|
+
# @return [Builder] A configured builder
|
|
131
|
+
def for(agent_class)
|
|
132
|
+
new(agent_class).tap do |builder|
|
|
133
|
+
# Always included - tenant resolution
|
|
134
|
+
builder.use(Middleware::Tenant)
|
|
135
|
+
|
|
136
|
+
# Budget checking (if enabled globally)
|
|
137
|
+
builder.use(Middleware::Budget) if budgets_enabled?
|
|
138
|
+
|
|
139
|
+
# Instrumentation (always - for tracking, must be before Cache)
|
|
140
|
+
builder.use(Middleware::Instrumentation)
|
|
141
|
+
|
|
142
|
+
# Caching (if enabled on the agent)
|
|
143
|
+
builder.use(Middleware::Cache) if cache_enabled?(agent_class)
|
|
144
|
+
|
|
145
|
+
# Reliability (if agent has retries or fallbacks configured)
|
|
146
|
+
builder.use(Middleware::Reliability) if reliability_enabled?(agent_class)
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# Returns an empty builder (no middleware)
|
|
151
|
+
#
|
|
152
|
+
# Useful for testing or when you want full control.
|
|
153
|
+
#
|
|
154
|
+
# @param agent_class [Class] The agent class
|
|
155
|
+
# @return [Builder] An empty builder
|
|
156
|
+
def empty(agent_class)
|
|
157
|
+
new(agent_class)
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
private
|
|
161
|
+
|
|
162
|
+
# Check if budgets are enabled globally
|
|
163
|
+
#
|
|
164
|
+
# @return [Boolean]
|
|
165
|
+
def budgets_enabled?
|
|
166
|
+
RubyLLM::Agents.configuration.budgets_enabled?
|
|
167
|
+
rescue StandardError
|
|
168
|
+
false
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
# Check if caching is enabled for an agent
|
|
172
|
+
#
|
|
173
|
+
# @param agent_class [Class] The agent class
|
|
174
|
+
# @return [Boolean]
|
|
175
|
+
def cache_enabled?(agent_class)
|
|
176
|
+
return false unless agent_class
|
|
177
|
+
|
|
178
|
+
agent_class.respond_to?(:cache_enabled?) && agent_class.cache_enabled?
|
|
179
|
+
rescue StandardError
|
|
180
|
+
false
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
# Check if reliability features are enabled for an agent
|
|
184
|
+
#
|
|
185
|
+
# An agent has reliability enabled if it has:
|
|
186
|
+
# - retries > 0, OR
|
|
187
|
+
# - fallback_models configured
|
|
188
|
+
#
|
|
189
|
+
# @param agent_class [Class] The agent class
|
|
190
|
+
# @return [Boolean]
|
|
191
|
+
def reliability_enabled?(agent_class)
|
|
192
|
+
return false unless agent_class
|
|
193
|
+
|
|
194
|
+
retries = if agent_class.respond_to?(:retries)
|
|
195
|
+
agent_class.retries
|
|
196
|
+
else
|
|
197
|
+
0
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
fallbacks = if agent_class.respond_to?(:fallback_models)
|
|
201
|
+
agent_class.fallback_models
|
|
202
|
+
else
|
|
203
|
+
[]
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
(retries.is_a?(Integer) && retries.positive?) ||
|
|
207
|
+
(fallbacks.is_a?(Array) && fallbacks.any?)
|
|
208
|
+
rescue StandardError
|
|
209
|
+
false
|
|
210
|
+
end
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
end
|
|
215
|
+
end
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
module Agents
|
|
5
|
+
module Pipeline
|
|
6
|
+
# Carries request/response data through the middleware pipeline.
|
|
7
|
+
#
|
|
8
|
+
# All data flows explicitly through this object - no hidden
|
|
9
|
+
# instance variables or implicit state. This makes the data flow
|
|
10
|
+
# visible and testable.
|
|
11
|
+
#
|
|
12
|
+
# @example Creating a context
|
|
13
|
+
# context = Context.new(
|
|
14
|
+
# input: "Hello world",
|
|
15
|
+
# agent_class: MyEmbedder,
|
|
16
|
+
# model: "text-embedding-3-small"
|
|
17
|
+
# )
|
|
18
|
+
#
|
|
19
|
+
# @example Accessing data set by middleware
|
|
20
|
+
# context.tenant_id # Set by Tenant middleware
|
|
21
|
+
# context.cached? # Set by Cache middleware
|
|
22
|
+
# context.duration_ms # Computed from timestamps
|
|
23
|
+
#
|
|
24
|
+
class Context
|
|
25
|
+
# Request data
|
|
26
|
+
attr_accessor :input, :model, :options
|
|
27
|
+
|
|
28
|
+
# Agent reference (for execution)
|
|
29
|
+
attr_accessor :agent_instance
|
|
30
|
+
|
|
31
|
+
# Tenant data (set by Tenant middleware, or passed in)
|
|
32
|
+
attr_accessor :tenant_id, :tenant_object, :tenant_config
|
|
33
|
+
|
|
34
|
+
# Execution tracking (set by Instrumentation middleware)
|
|
35
|
+
attr_accessor :started_at, :completed_at, :attempt, :attempts_made, :execution_id
|
|
36
|
+
|
|
37
|
+
# Result data (set by core execute method)
|
|
38
|
+
attr_accessor :output, :error, :cached
|
|
39
|
+
|
|
40
|
+
# Cost tracking
|
|
41
|
+
attr_accessor :input_tokens, :output_tokens, :input_cost, :output_cost, :total_cost
|
|
42
|
+
|
|
43
|
+
# Response metadata
|
|
44
|
+
attr_accessor :model_used, :finish_reason, :time_to_first_token_ms
|
|
45
|
+
|
|
46
|
+
# Streaming support
|
|
47
|
+
attr_accessor :stream_block, :skip_cache
|
|
48
|
+
|
|
49
|
+
# Agent metadata
|
|
50
|
+
attr_reader :agent_class, :agent_type
|
|
51
|
+
|
|
52
|
+
# Creates a new pipeline context
|
|
53
|
+
#
|
|
54
|
+
# @param input [Object] The input data for the agent
|
|
55
|
+
# @param agent_class [Class] The agent class being executed
|
|
56
|
+
# @param agent_instance [Object, nil] The agent instance
|
|
57
|
+
# @param model [String, nil] Override model (defaults to agent_class.model)
|
|
58
|
+
# @param tenant [Hash, Object, nil] Raw tenant (resolved by Tenant middleware)
|
|
59
|
+
# @param skip_cache [Boolean] Whether to skip caching
|
|
60
|
+
# @param stream_block [Proc, nil] Block for streaming
|
|
61
|
+
# @param options [Hash] Additional options passed to the agent
|
|
62
|
+
def initialize(input:, agent_class:, agent_instance: nil, model: nil, tenant: nil, skip_cache: false, stream_block: nil, **options)
|
|
63
|
+
@input = input
|
|
64
|
+
@agent_class = agent_class
|
|
65
|
+
@agent_instance = agent_instance
|
|
66
|
+
@agent_type = extract_agent_type(agent_class)
|
|
67
|
+
@model = model || extract_model(agent_class)
|
|
68
|
+
|
|
69
|
+
# Store tenant in options for middleware to resolve
|
|
70
|
+
@options = options.merge(tenant: tenant).compact
|
|
71
|
+
|
|
72
|
+
# Tenant fields (set by Tenant middleware)
|
|
73
|
+
@tenant_id = nil
|
|
74
|
+
@tenant_object = nil
|
|
75
|
+
@tenant_config = nil
|
|
76
|
+
|
|
77
|
+
# Execution options
|
|
78
|
+
@skip_cache = skip_cache
|
|
79
|
+
@stream_block = stream_block
|
|
80
|
+
|
|
81
|
+
# Initialize tracking fields
|
|
82
|
+
@attempt = 0
|
|
83
|
+
@attempts_made = 0
|
|
84
|
+
@cached = false
|
|
85
|
+
@metadata = {}
|
|
86
|
+
|
|
87
|
+
# Initialize cost fields
|
|
88
|
+
@input_tokens = 0
|
|
89
|
+
@output_tokens = 0
|
|
90
|
+
@input_cost = 0.0
|
|
91
|
+
@output_cost = 0.0
|
|
92
|
+
@total_cost = 0.0
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Duration in milliseconds
|
|
96
|
+
#
|
|
97
|
+
# @return [Integer, nil] Duration in ms, or nil if not yet completed
|
|
98
|
+
def duration_ms
|
|
99
|
+
return nil unless @started_at && @completed_at
|
|
100
|
+
|
|
101
|
+
((@completed_at - @started_at) * 1000).to_i
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# Was the result served from cache?
|
|
105
|
+
#
|
|
106
|
+
# @return [Boolean]
|
|
107
|
+
def cached?
|
|
108
|
+
@cached == true
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# Did execution succeed?
|
|
112
|
+
#
|
|
113
|
+
# @return [Boolean]
|
|
114
|
+
def success?
|
|
115
|
+
@error.nil? && !@output.nil?
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# Did execution fail?
|
|
119
|
+
#
|
|
120
|
+
# @return [Boolean]
|
|
121
|
+
def failed?
|
|
122
|
+
!@error.nil?
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# Total tokens used (input + output)
|
|
126
|
+
#
|
|
127
|
+
# @return [Integer]
|
|
128
|
+
def total_tokens
|
|
129
|
+
(@input_tokens || 0) + (@output_tokens || 0)
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# Custom metadata storage - read
|
|
133
|
+
#
|
|
134
|
+
# @param key [Symbol, String] The metadata key
|
|
135
|
+
# @return [Object] The stored value
|
|
136
|
+
def [](key)
|
|
137
|
+
@metadata[key]
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# Custom metadata storage - write
|
|
141
|
+
#
|
|
142
|
+
# @param key [Symbol, String] The metadata key
|
|
143
|
+
# @param value [Object] The value to store
|
|
144
|
+
def []=(key, value)
|
|
145
|
+
@metadata[key] = value
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# Returns all custom metadata
|
|
149
|
+
#
|
|
150
|
+
# @return [Hash] The metadata hash
|
|
151
|
+
def metadata
|
|
152
|
+
@metadata.dup
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
# Convert to hash for logging/recording
|
|
156
|
+
#
|
|
157
|
+
# @return [Hash] Hash representation of the context
|
|
158
|
+
def to_h
|
|
159
|
+
{
|
|
160
|
+
agent_class: @agent_class&.name,
|
|
161
|
+
agent_type: @agent_type,
|
|
162
|
+
model: @model,
|
|
163
|
+
model_used: @model_used,
|
|
164
|
+
tenant_id: @tenant_id,
|
|
165
|
+
duration_ms: duration_ms,
|
|
166
|
+
cached: cached?,
|
|
167
|
+
success: success?,
|
|
168
|
+
input_tokens: @input_tokens,
|
|
169
|
+
output_tokens: @output_tokens,
|
|
170
|
+
total_tokens: total_tokens,
|
|
171
|
+
input_cost: @input_cost,
|
|
172
|
+
output_cost: @output_cost,
|
|
173
|
+
total_cost: @total_cost,
|
|
174
|
+
finish_reason: @finish_reason,
|
|
175
|
+
time_to_first_token_ms: @time_to_first_token_ms,
|
|
176
|
+
attempts_made: @attempts_made,
|
|
177
|
+
error_class: @error&.class&.name,
|
|
178
|
+
error_message: @error&.message
|
|
179
|
+
}.compact
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
# Creates a duplicate context for retry attempts
|
|
183
|
+
#
|
|
184
|
+
# @return [Context] A new context with the same input but reset state
|
|
185
|
+
def dup_for_retry
|
|
186
|
+
# Extract tenant from options since dup_for_retry is called after middleware
|
|
187
|
+
# has already resolved it - we want to preserve the resolved state
|
|
188
|
+
opts_without_tenant = @options.except(:tenant)
|
|
189
|
+
|
|
190
|
+
new_ctx = self.class.new(
|
|
191
|
+
input: @input,
|
|
192
|
+
agent_class: @agent_class,
|
|
193
|
+
agent_instance: @agent_instance,
|
|
194
|
+
model: @model,
|
|
195
|
+
skip_cache: @skip_cache,
|
|
196
|
+
stream_block: @stream_block,
|
|
197
|
+
**opts_without_tenant
|
|
198
|
+
)
|
|
199
|
+
# Preserve resolved tenant state
|
|
200
|
+
new_ctx.tenant_id = @tenant_id
|
|
201
|
+
new_ctx.tenant_object = @tenant_object
|
|
202
|
+
new_ctx.tenant_config = @tenant_config
|
|
203
|
+
new_ctx.started_at = @started_at
|
|
204
|
+
new_ctx.attempts_made = @attempts_made
|
|
205
|
+
new_ctx
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
private
|
|
209
|
+
|
|
210
|
+
# Extracts agent_type from the agent class
|
|
211
|
+
#
|
|
212
|
+
# @param agent_class [Class] The agent class
|
|
213
|
+
# @return [Symbol, nil] The agent type
|
|
214
|
+
def extract_agent_type(agent_class)
|
|
215
|
+
return nil unless agent_class
|
|
216
|
+
|
|
217
|
+
if agent_class.respond_to?(:agent_type)
|
|
218
|
+
agent_class.agent_type
|
|
219
|
+
else
|
|
220
|
+
# Infer from class name as fallback
|
|
221
|
+
infer_agent_type(agent_class)
|
|
222
|
+
end
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
# Infers agent type from class name
|
|
226
|
+
#
|
|
227
|
+
# @param agent_class [Class] The agent class
|
|
228
|
+
# @return [Symbol] The inferred agent type
|
|
229
|
+
def infer_agent_type(agent_class)
|
|
230
|
+
name = agent_class.name.to_s.split("::").last.to_s.downcase
|
|
231
|
+
|
|
232
|
+
case name
|
|
233
|
+
when /embed/ then :embedding
|
|
234
|
+
when /image/, /generator/, /analyzer/, /editor/, /transform/, /upscale/, /variat/, /background/
|
|
235
|
+
:image
|
|
236
|
+
when /transcrib/, /speak/ then :audio
|
|
237
|
+
when /moderat/ then :moderation
|
|
238
|
+
else :conversation
|
|
239
|
+
end
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
# Extracts model from agent class
|
|
243
|
+
#
|
|
244
|
+
# @param agent_class [Class] The agent class
|
|
245
|
+
# @return [String, nil] The model identifier
|
|
246
|
+
def extract_model(agent_class)
|
|
247
|
+
return nil unless agent_class
|
|
248
|
+
return agent_class.model if agent_class.respond_to?(:model)
|
|
249
|
+
|
|
250
|
+
nil
|
|
251
|
+
end
|
|
252
|
+
end
|
|
253
|
+
end
|
|
254
|
+
end
|
|
255
|
+
end
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
module Agents
|
|
5
|
+
module Pipeline
|
|
6
|
+
# Wraps an agent's execute method to work with the pipeline.
|
|
7
|
+
#
|
|
8
|
+
# This is the "core" that middleware wraps around. It's the final
|
|
9
|
+
# handler in the chain that actually performs the agent's work.
|
|
10
|
+
#
|
|
11
|
+
# The Executor adapts the agent's #execute method to the middleware
|
|
12
|
+
# interface (call(context) -> context).
|
|
13
|
+
#
|
|
14
|
+
# @example Basic usage
|
|
15
|
+
# executor = Executor.new(agent)
|
|
16
|
+
# context = Context.new(input: "hello", agent_class: MyAgent)
|
|
17
|
+
# result_context = executor.call(context)
|
|
18
|
+
#
|
|
19
|
+
# @example With pipeline
|
|
20
|
+
# pipeline = Builder.for(MyAgent).build(Executor.new(agent))
|
|
21
|
+
# result = pipeline.call(context)
|
|
22
|
+
#
|
|
23
|
+
# @example Convenience class method
|
|
24
|
+
# result_context = Executor.execute(context)
|
|
25
|
+
#
|
|
26
|
+
class Executor
|
|
27
|
+
# Execute a context through the full pipeline
|
|
28
|
+
#
|
|
29
|
+
# Builds the middleware stack based on the agent's configuration,
|
|
30
|
+
# then executes the context through it.
|
|
31
|
+
#
|
|
32
|
+
# @param context [Context] The execution context
|
|
33
|
+
# @return [Context] The context with output set
|
|
34
|
+
def self.execute(context)
|
|
35
|
+
agent_instance = context.agent_instance
|
|
36
|
+
core = new(agent_instance)
|
|
37
|
+
pipeline = Builder.for(context.agent_class).build(core)
|
|
38
|
+
pipeline.call(context)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# @param agent [Object] The agent instance with an #execute method
|
|
42
|
+
def initialize(agent)
|
|
43
|
+
@agent = agent
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Execute the agent's core logic
|
|
47
|
+
#
|
|
48
|
+
# Calls the agent's #execute method with the context.
|
|
49
|
+
# The agent is expected to set context.output with the result.
|
|
50
|
+
#
|
|
51
|
+
# @param context [Context] The execution context
|
|
52
|
+
# @return [Context] The context with output set
|
|
53
|
+
def call(context)
|
|
54
|
+
@agent.execute(context)
|
|
55
|
+
context
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Lambda-based executor for simple cases
|
|
60
|
+
#
|
|
61
|
+
# Allows wrapping a lambda/proc as the core executor,
|
|
62
|
+
# useful for testing or simple agents.
|
|
63
|
+
#
|
|
64
|
+
# @example
|
|
65
|
+
# executor = LambdaExecutor.new(->(ctx) {
|
|
66
|
+
# ctx.output = "Hello, #{ctx.input}!"
|
|
67
|
+
# })
|
|
68
|
+
#
|
|
69
|
+
class LambdaExecutor
|
|
70
|
+
# @param callable [#call] A lambda/proc that takes a context
|
|
71
|
+
def initialize(callable)
|
|
72
|
+
@callable = callable
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Execute the lambda with the context
|
|
76
|
+
#
|
|
77
|
+
# @param context [Context] The execution context
|
|
78
|
+
# @return [Context] The context (possibly modified by the lambda)
|
|
79
|
+
def call(context)
|
|
80
|
+
@callable.call(context)
|
|
81
|
+
context
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|