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.
Files changed (208) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +225 -34
  3. data/app/controllers/ruby_llm/agents/agents_controller.rb +136 -16
  4. data/app/controllers/ruby_llm/agents/api_configurations_controller.rb +214 -0
  5. data/app/controllers/ruby_llm/agents/dashboard_controller.rb +29 -9
  6. data/app/controllers/ruby_llm/agents/{settings_controller.rb → system_config_controller.rb} +3 -3
  7. data/app/controllers/ruby_llm/agents/tenants_controller.rb +109 -0
  8. data/app/controllers/ruby_llm/agents/workflows_controller.rb +355 -0
  9. data/app/helpers/ruby_llm/agents/application_helper.rb +25 -0
  10. data/app/models/ruby_llm/agents/api_configuration.rb +386 -0
  11. data/app/models/ruby_llm/agents/execution.rb +3 -0
  12. data/app/models/ruby_llm/agents/tenant_budget.rb +112 -14
  13. data/app/services/ruby_llm/agents/agent_registry.rb +51 -12
  14. data/app/views/layouts/ruby_llm/agents/application.html.erb +5 -30
  15. data/app/views/ruby_llm/agents/agents/_agent.html.erb +13 -1
  16. data/app/views/ruby_llm/agents/agents/_config_agent.html.erb +235 -0
  17. data/app/views/ruby_llm/agents/agents/_config_embedder.html.erb +70 -0
  18. data/app/views/ruby_llm/agents/agents/_config_image_generator.html.erb +152 -0
  19. data/app/views/ruby_llm/agents/agents/_config_moderator.html.erb +63 -0
  20. data/app/views/ruby_llm/agents/agents/_config_speaker.html.erb +108 -0
  21. data/app/views/ruby_llm/agents/agents/_config_transcriber.html.erb +91 -0
  22. data/app/views/ruby_llm/agents/agents/_workflow.html.erb +1 -1
  23. data/app/views/ruby_llm/agents/agents/index.html.erb +74 -9
  24. data/app/views/ruby_llm/agents/agents/show.html.erb +18 -378
  25. data/app/views/ruby_llm/agents/api_configurations/_api_key_field.html.erb +34 -0
  26. data/app/views/ruby_llm/agents/api_configurations/_form.html.erb +288 -0
  27. data/app/views/ruby_llm/agents/api_configurations/edit.html.erb +95 -0
  28. data/app/views/ruby_llm/agents/api_configurations/edit_tenant.html.erb +97 -0
  29. data/app/views/ruby_llm/agents/api_configurations/show.html.erb +211 -0
  30. data/app/views/ruby_llm/agents/api_configurations/tenant.html.erb +179 -0
  31. data/app/views/ruby_llm/agents/dashboard/_action_center.html.erb +1 -1
  32. data/app/views/ruby_llm/agents/dashboard/_agent_comparison.html.erb +269 -15
  33. data/app/views/ruby_llm/agents/executions/show.html.erb +98 -0
  34. data/app/views/ruby_llm/agents/shared/_agent_type_badge.html.erb +93 -0
  35. data/app/views/ruby_llm/agents/{settings → system_config}/show.html.erb +1 -1
  36. data/app/views/ruby_llm/agents/tenants/_form.html.erb +150 -0
  37. data/app/views/ruby_llm/agents/tenants/edit.html.erb +13 -0
  38. data/app/views/ruby_llm/agents/tenants/index.html.erb +129 -0
  39. data/app/views/ruby_llm/agents/tenants/show.html.erb +374 -0
  40. data/app/views/ruby_llm/agents/workflows/_step_performance.html.erb +236 -0
  41. data/app/views/ruby_llm/agents/workflows/_structure_parallel.html.erb +76 -0
  42. data/app/views/ruby_llm/agents/workflows/_structure_pipeline.html.erb +74 -0
  43. data/app/views/ruby_llm/agents/workflows/_structure_router.html.erb +108 -0
  44. data/app/views/ruby_llm/agents/workflows/show.html.erb +442 -0
  45. data/config/routes.rb +13 -1
  46. data/lib/generators/ruby_llm_agents/agent_generator.rb +56 -7
  47. data/lib/generators/ruby_llm_agents/api_configuration_generator.rb +100 -0
  48. data/lib/generators/ruby_llm_agents/background_remover_generator.rb +110 -0
  49. data/lib/generators/ruby_llm_agents/embedder_generator.rb +107 -0
  50. data/lib/generators/ruby_llm_agents/image_analyzer_generator.rb +115 -0
  51. data/lib/generators/ruby_llm_agents/image_editor_generator.rb +108 -0
  52. data/lib/generators/ruby_llm_agents/image_generator_generator.rb +116 -0
  53. data/lib/generators/ruby_llm_agents/image_pipeline_generator.rb +178 -0
  54. data/lib/generators/ruby_llm_agents/image_transformer_generator.rb +109 -0
  55. data/lib/generators/ruby_llm_agents/image_upscaler_generator.rb +103 -0
  56. data/lib/generators/ruby_llm_agents/image_variator_generator.rb +102 -0
  57. data/lib/generators/ruby_llm_agents/install_generator.rb +76 -4
  58. data/lib/generators/ruby_llm_agents/restructure_generator.rb +292 -0
  59. data/lib/generators/ruby_llm_agents/speaker_generator.rb +121 -0
  60. data/lib/generators/ruby_llm_agents/templates/add_execution_type_migration.rb.tt +8 -0
  61. data/lib/generators/ruby_llm_agents/templates/agent.rb.tt +99 -84
  62. data/lib/generators/ruby_llm_agents/templates/application_agent.rb.tt +42 -40
  63. data/lib/generators/ruby_llm_agents/templates/application_background_remover.rb.tt +26 -0
  64. data/lib/generators/ruby_llm_agents/templates/application_embedder.rb.tt +50 -0
  65. data/lib/generators/ruby_llm_agents/templates/application_image_analyzer.rb.tt +26 -0
  66. data/lib/generators/ruby_llm_agents/templates/application_image_editor.rb.tt +20 -0
  67. data/lib/generators/ruby_llm_agents/templates/application_image_generator.rb.tt +38 -0
  68. data/lib/generators/ruby_llm_agents/templates/application_image_pipeline.rb.tt +139 -0
  69. data/lib/generators/ruby_llm_agents/templates/application_image_transformer.rb.tt +21 -0
  70. data/lib/generators/ruby_llm_agents/templates/application_image_upscaler.rb.tt +20 -0
  71. data/lib/generators/ruby_llm_agents/templates/application_image_variator.rb.tt +20 -0
  72. data/lib/generators/ruby_llm_agents/templates/application_speaker.rb.tt +49 -0
  73. data/lib/generators/ruby_llm_agents/templates/application_transcriber.rb.tt +53 -0
  74. data/lib/generators/ruby_llm_agents/templates/background_remover.rb.tt +44 -0
  75. data/lib/generators/ruby_llm_agents/templates/create_api_configurations_migration.rb.tt +90 -0
  76. data/lib/generators/ruby_llm_agents/templates/embedder.rb.tt +41 -0
  77. data/lib/generators/ruby_llm_agents/templates/image_analyzer.rb.tt +45 -0
  78. data/lib/generators/ruby_llm_agents/templates/image_editor.rb.tt +35 -0
  79. data/lib/generators/ruby_llm_agents/templates/image_generator.rb.tt +47 -0
  80. data/lib/generators/ruby_llm_agents/templates/image_pipeline.rb.tt +50 -0
  81. data/lib/generators/ruby_llm_agents/templates/image_transformer.rb.tt +44 -0
  82. data/lib/generators/ruby_llm_agents/templates/image_upscaler.rb.tt +38 -0
  83. data/lib/generators/ruby_llm_agents/templates/image_variator.rb.tt +33 -0
  84. data/lib/generators/ruby_llm_agents/templates/skills/AGENTS.md.tt +228 -0
  85. data/lib/generators/ruby_llm_agents/templates/skills/BACKGROUND_REMOVERS.md.tt +131 -0
  86. data/lib/generators/ruby_llm_agents/templates/skills/EMBEDDERS.md.tt +255 -0
  87. data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_ANALYZERS.md.tt +120 -0
  88. data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_EDITORS.md.tt +102 -0
  89. data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_GENERATORS.md.tt +282 -0
  90. data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_PIPELINES.md.tt +228 -0
  91. data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_TRANSFORMERS.md.tt +120 -0
  92. data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_UPSCALERS.md.tt +110 -0
  93. data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_VARIATORS.md.tt +120 -0
  94. data/lib/generators/ruby_llm_agents/templates/skills/SPEAKERS.md.tt +212 -0
  95. data/lib/generators/ruby_llm_agents/templates/skills/TOOLS.md.tt +227 -0
  96. data/lib/generators/ruby_llm_agents/templates/skills/TRANSCRIBERS.md.tt +251 -0
  97. data/lib/generators/ruby_llm_agents/templates/skills/WORKFLOWS.md.tt +300 -0
  98. data/lib/generators/ruby_llm_agents/templates/speaker.rb.tt +56 -0
  99. data/lib/generators/ruby_llm_agents/templates/transcriber.rb.tt +51 -0
  100. data/lib/generators/ruby_llm_agents/transcriber_generator.rb +107 -0
  101. data/lib/generators/ruby_llm_agents/upgrade_generator.rb +152 -1
  102. data/lib/ruby_llm/agents/audio/speaker.rb +553 -0
  103. data/lib/ruby_llm/agents/audio/transcriber.rb +669 -0
  104. data/lib/ruby_llm/agents/base_agent.rb +675 -0
  105. data/lib/ruby_llm/agents/core/base/moderation_dsl.rb +181 -0
  106. data/lib/ruby_llm/agents/core/base/moderation_execution.rb +274 -0
  107. data/lib/ruby_llm/agents/core/base.rb +135 -0
  108. data/lib/ruby_llm/agents/core/configuration.rb +981 -0
  109. data/lib/ruby_llm/agents/core/errors.rb +150 -0
  110. data/lib/ruby_llm/agents/{instrumentation.rb → core/instrumentation.rb} +93 -4
  111. data/lib/ruby_llm/agents/core/llm_tenant.rb +358 -0
  112. data/lib/ruby_llm/agents/core/resolved_config.rb +348 -0
  113. data/lib/ruby_llm/agents/{version.rb → core/version.rb} +1 -1
  114. data/lib/ruby_llm/agents/dsl/base.rb +110 -0
  115. data/lib/ruby_llm/agents/dsl/caching.rb +142 -0
  116. data/lib/ruby_llm/agents/dsl/reliability.rb +307 -0
  117. data/lib/ruby_llm/agents/dsl.rb +41 -0
  118. data/lib/ruby_llm/agents/image/analyzer/dsl.rb +130 -0
  119. data/lib/ruby_llm/agents/image/analyzer/execution.rb +402 -0
  120. data/lib/ruby_llm/agents/image/analyzer.rb +90 -0
  121. data/lib/ruby_llm/agents/image/background_remover/dsl.rb +154 -0
  122. data/lib/ruby_llm/agents/image/background_remover/execution.rb +240 -0
  123. data/lib/ruby_llm/agents/image/background_remover.rb +89 -0
  124. data/lib/ruby_llm/agents/image/concerns/image_operation_dsl.rb +91 -0
  125. data/lib/ruby_llm/agents/image/concerns/image_operation_execution.rb +165 -0
  126. data/lib/ruby_llm/agents/image/editor/dsl.rb +56 -0
  127. data/lib/ruby_llm/agents/image/editor/execution.rb +207 -0
  128. data/lib/ruby_llm/agents/image/editor.rb +92 -0
  129. data/lib/ruby_llm/agents/image/generator/active_storage_support.rb +127 -0
  130. data/lib/ruby_llm/agents/image/generator/content_policy.rb +95 -0
  131. data/lib/ruby_llm/agents/image/generator/pricing.rb +353 -0
  132. data/lib/ruby_llm/agents/image/generator/templates.rb +124 -0
  133. data/lib/ruby_llm/agents/image/generator.rb +455 -0
  134. data/lib/ruby_llm/agents/image/pipeline/dsl.rb +213 -0
  135. data/lib/ruby_llm/agents/image/pipeline/execution.rb +382 -0
  136. data/lib/ruby_llm/agents/image/pipeline.rb +97 -0
  137. data/lib/ruby_llm/agents/image/transformer/dsl.rb +148 -0
  138. data/lib/ruby_llm/agents/image/transformer/execution.rb +223 -0
  139. data/lib/ruby_llm/agents/image/transformer.rb +95 -0
  140. data/lib/ruby_llm/agents/image/upscaler/dsl.rb +83 -0
  141. data/lib/ruby_llm/agents/image/upscaler/execution.rb +219 -0
  142. data/lib/ruby_llm/agents/image/upscaler.rb +81 -0
  143. data/lib/ruby_llm/agents/image/variator/dsl.rb +62 -0
  144. data/lib/ruby_llm/agents/image/variator/execution.rb +189 -0
  145. data/lib/ruby_llm/agents/image/variator.rb +80 -0
  146. data/lib/ruby_llm/agents/{alert_manager.rb → infrastructure/alert_manager.rb} +17 -22
  147. data/lib/ruby_llm/agents/infrastructure/budget/budget_query.rb +145 -0
  148. data/lib/ruby_llm/agents/infrastructure/budget/config_resolver.rb +149 -0
  149. data/lib/ruby_llm/agents/infrastructure/budget/forecaster.rb +68 -0
  150. data/lib/ruby_llm/agents/infrastructure/budget/spend_recorder.rb +279 -0
  151. data/lib/ruby_llm/agents/infrastructure/budget_tracker.rb +275 -0
  152. data/lib/ruby_llm/agents/{execution_logger_job.rb → infrastructure/execution_logger_job.rb} +17 -1
  153. data/lib/ruby_llm/agents/{reliability → infrastructure/reliability}/executor.rb +2 -1
  154. data/lib/ruby_llm/agents/{reliability → infrastructure/reliability}/retry_strategy.rb +9 -3
  155. data/lib/ruby_llm/agents/{reliability.rb → infrastructure/reliability.rb} +11 -21
  156. data/lib/ruby_llm/agents/pipeline/builder.rb +215 -0
  157. data/lib/ruby_llm/agents/pipeline/context.rb +255 -0
  158. data/lib/ruby_llm/agents/pipeline/executor.rb +86 -0
  159. data/lib/ruby_llm/agents/pipeline/middleware/base.rb +124 -0
  160. data/lib/ruby_llm/agents/pipeline/middleware/budget.rb +95 -0
  161. data/lib/ruby_llm/agents/pipeline/middleware/cache.rb +171 -0
  162. data/lib/ruby_llm/agents/pipeline/middleware/instrumentation.rb +415 -0
  163. data/lib/ruby_llm/agents/pipeline/middleware/reliability.rb +276 -0
  164. data/lib/ruby_llm/agents/pipeline/middleware/tenant.rb +196 -0
  165. data/lib/ruby_llm/agents/pipeline.rb +68 -0
  166. data/lib/ruby_llm/agents/{engine.rb → rails/engine.rb} +79 -10
  167. data/lib/ruby_llm/agents/results/background_removal_result.rb +286 -0
  168. data/lib/ruby_llm/agents/{result.rb → results/base.rb} +73 -1
  169. data/lib/ruby_llm/agents/results/embedding_result.rb +243 -0
  170. data/lib/ruby_llm/agents/results/image_analysis_result.rb +314 -0
  171. data/lib/ruby_llm/agents/results/image_edit_result.rb +250 -0
  172. data/lib/ruby_llm/agents/results/image_generation_result.rb +346 -0
  173. data/lib/ruby_llm/agents/results/image_pipeline_result.rb +399 -0
  174. data/lib/ruby_llm/agents/results/image_transform_result.rb +251 -0
  175. data/lib/ruby_llm/agents/results/image_upscale_result.rb +255 -0
  176. data/lib/ruby_llm/agents/results/image_variation_result.rb +237 -0
  177. data/lib/ruby_llm/agents/results/moderation_result.rb +158 -0
  178. data/lib/ruby_llm/agents/results/speech_result.rb +338 -0
  179. data/lib/ruby_llm/agents/results/transcription_result.rb +408 -0
  180. data/lib/ruby_llm/agents/text/embedder.rb +444 -0
  181. data/lib/ruby_llm/agents/text/moderator.rb +237 -0
  182. data/lib/ruby_llm/agents/workflow/async.rb +220 -0
  183. data/lib/ruby_llm/agents/workflow/async_executor.rb +156 -0
  184. data/lib/ruby_llm/agents/{workflow.rb → workflow/orchestrator.rb} +6 -5
  185. data/lib/ruby_llm/agents/workflow/parallel.rb +34 -17
  186. data/lib/ruby_llm/agents/workflow/thread_pool.rb +185 -0
  187. data/lib/ruby_llm/agents.rb +86 -20
  188. metadata +189 -35
  189. data/lib/ruby_llm/agents/base/caching.rb +0 -40
  190. data/lib/ruby_llm/agents/base/cost_calculation.rb +0 -105
  191. data/lib/ruby_llm/agents/base/dsl.rb +0 -324
  192. data/lib/ruby_llm/agents/base/execution.rb +0 -283
  193. data/lib/ruby_llm/agents/base/reliability_dsl.rb +0 -82
  194. data/lib/ruby_llm/agents/base/reliability_execution.rb +0 -136
  195. data/lib/ruby_llm/agents/base/response_building.rb +0 -86
  196. data/lib/ruby_llm/agents/base/tool_tracking.rb +0 -57
  197. data/lib/ruby_llm/agents/base.rb +0 -209
  198. data/lib/ruby_llm/agents/budget_tracker.rb +0 -471
  199. data/lib/ruby_llm/agents/configuration.rb +0 -357
  200. /data/lib/ruby_llm/agents/{deprecations.rb → core/deprecations.rb} +0 -0
  201. /data/lib/ruby_llm/agents/{inflections.rb → core/inflections.rb} +0 -0
  202. /data/lib/ruby_llm/agents/{attempt_tracker.rb → infrastructure/attempt_tracker.rb} +0 -0
  203. /data/lib/ruby_llm/agents/{cache_helper.rb → infrastructure/cache_helper.rb} +0 -0
  204. /data/lib/ruby_llm/agents/{circuit_breaker.rb → infrastructure/circuit_breaker.rb} +0 -0
  205. /data/lib/ruby_llm/agents/{redactor.rb → infrastructure/redactor.rb} +0 -0
  206. /data/lib/ruby_llm/agents/{reliability → infrastructure/reliability}/breaker_manager.rb +0 -0
  207. /data/lib/ruby_llm/agents/{reliability → infrastructure/reliability}/execution_constraints.rb +0 -0
  208. /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
- def initialize(max: 0, backoff: :exponential, base: 0.4, max_delay: 3.0, on: [])
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?(error, custom_errors: custom_errors)
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) } || retryable_by_message?(error)
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
- @retryable_patterns ||= [
150
- "rate limit",
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