ruby_llm-agents 1.3.4 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (191) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +112 -336
  3. data/app/controllers/concerns/ruby_llm/agents/sortable.rb +0 -1
  4. data/app/controllers/ruby_llm/agents/agents_controller.rb +5 -56
  5. data/app/controllers/ruby_llm/agents/dashboard_controller.rb +22 -106
  6. data/app/controllers/ruby_llm/agents/executions_controller.rb +4 -114
  7. data/app/controllers/ruby_llm/agents/tenants_controller.rb +30 -2
  8. data/app/helpers/ruby_llm/agents/application_helper.rb +19 -53
  9. data/app/models/ruby_llm/agents/execution/analytics.rb +13 -54
  10. data/app/models/ruby_llm/agents/execution/scopes.rb +61 -14
  11. data/app/models/ruby_llm/agents/execution.rb +52 -12
  12. data/app/models/ruby_llm/agents/execution_detail.rb +18 -0
  13. data/app/models/ruby_llm/agents/tenant/budgetable.rb +132 -24
  14. data/app/models/ruby_llm/agents/tenant/incrementable.rb +117 -0
  15. data/app/models/ruby_llm/agents/tenant/resettable.rb +128 -0
  16. data/app/models/ruby_llm/agents/tenant/trackable.rb +46 -12
  17. data/app/models/ruby_llm/agents/tenant.rb +2 -3
  18. data/app/models/ruby_llm/agents/tenant_budget.rb +6 -3
  19. data/app/services/ruby_llm/agents/agent_registry.rb +6 -112
  20. data/app/views/layouts/ruby_llm/agents/application.html.erb +89 -252
  21. data/app/views/ruby_llm/agents/agents/_config_agent.html.erb +71 -218
  22. data/app/views/ruby_llm/agents/agents/_config_embedder.html.erb +20 -63
  23. data/app/views/ruby_llm/agents/agents/_config_image_generator.html.erb +44 -131
  24. data/app/views/ruby_llm/agents/agents/_config_moderator.html.erb +16 -57
  25. data/app/views/ruby_llm/agents/agents/_config_speaker.html.erb +39 -104
  26. data/app/views/ruby_llm/agents/agents/_config_transcriber.html.erb +29 -82
  27. data/app/views/ruby_llm/agents/agents/_empty_state.html.erb +4 -14
  28. data/app/views/ruby_llm/agents/agents/index.html.erb +105 -274
  29. data/app/views/ruby_llm/agents/agents/show.html.erb +248 -378
  30. data/app/views/ruby_llm/agents/dashboard/_action_center.html.erb +29 -52
  31. data/app/views/ruby_llm/agents/dashboard/_tenant_budget.html.erb +73 -99
  32. data/app/views/ruby_llm/agents/dashboard/index.html.erb +228 -433
  33. data/app/views/ruby_llm/agents/executions/_execution.html.erb +1 -1
  34. data/app/views/ruby_llm/agents/executions/_filters.html.erb +4 -25
  35. data/app/views/ruby_llm/agents/executions/_list.html.erb +111 -152
  36. data/app/views/ruby_llm/agents/executions/index.html.erb +5 -7
  37. data/app/views/ruby_llm/agents/executions/show.html.erb +526 -1037
  38. data/app/views/ruby_llm/agents/shared/_agent_type_badge.html.erb +5 -21
  39. data/app/views/ruby_llm/agents/shared/_executions_table.html.erb +70 -191
  40. data/app/views/ruby_llm/agents/shared/_filter_dropdown.html.erb +16 -44
  41. data/app/views/ruby_llm/agents/shared/_select_dropdown.html.erb +12 -41
  42. data/app/views/ruby_llm/agents/shared/_status_badge.html.erb +11 -65
  43. data/app/views/ruby_llm/agents/shared/_tenant_filter.html.erb +6 -5
  44. data/app/views/ruby_llm/agents/system_config/show.html.erb +240 -351
  45. data/app/views/ruby_llm/agents/tenants/_form.html.erb +67 -77
  46. data/app/views/ruby_llm/agents/tenants/edit.html.erb +7 -9
  47. data/app/views/ruby_llm/agents/tenants/index.html.erb +100 -122
  48. data/app/views/ruby_llm/agents/tenants/show.html.erb +146 -336
  49. data/config/routes.rb +0 -13
  50. data/lib/generators/ruby_llm_agents/install_generator.rb +13 -17
  51. data/lib/generators/ruby_llm_agents/migrate_structure_generator.rb +2 -12
  52. data/lib/generators/ruby_llm_agents/restructure_generator.rb +0 -2
  53. data/lib/generators/ruby_llm_agents/templates/add_usage_counters_to_tenants_migration.rb.tt +37 -0
  54. data/lib/generators/ruby_llm_agents/templates/agent.rb.tt +1 -2
  55. data/lib/generators/ruby_llm_agents/templates/application_agent.rb.tt +1 -1
  56. data/lib/generators/ruby_llm_agents/templates/application_image_pipeline.rb.tt +0 -1
  57. data/lib/generators/ruby_llm_agents/templates/create_execution_details_migration.rb.tt +27 -0
  58. data/lib/generators/ruby_llm_agents/templates/create_tenants_migration.rb.tt +25 -0
  59. data/lib/generators/ruby_llm_agents/templates/image_pipeline.rb.tt +0 -1
  60. data/lib/generators/ruby_llm_agents/templates/initializer.rb.tt +33 -12
  61. data/lib/generators/ruby_llm_agents/templates/migration.rb.tt +40 -71
  62. data/lib/generators/ruby_llm_agents/templates/remove_agent_version_migration.rb.tt +13 -0
  63. data/lib/generators/ruby_llm_agents/templates/remove_workflow_columns_migration.rb.tt +19 -0
  64. data/lib/generators/ruby_llm_agents/templates/skills/AGENTS.md.tt +2 -4
  65. data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_PIPELINES.md.tt +0 -1
  66. data/lib/generators/ruby_llm_agents/templates/split_execution_details_migration.rb.tt +232 -0
  67. data/lib/generators/ruby_llm_agents/upgrade_generator.rb +77 -259
  68. data/lib/ruby_llm/agents/audio/speaker.rb +0 -1
  69. data/lib/ruby_llm/agents/audio/transcriber.rb +0 -1
  70. data/lib/ruby_llm/agents/base_agent.rb +54 -23
  71. data/lib/ruby_llm/agents/core/base/callbacks.rb +142 -0
  72. data/lib/ruby_llm/agents/core/base.rb +23 -55
  73. data/lib/ruby_llm/agents/core/configuration.rb +97 -117
  74. data/lib/ruby_llm/agents/core/errors.rb +0 -58
  75. data/lib/ruby_llm/agents/core/instrumentation.rb +157 -110
  76. data/lib/ruby_llm/agents/core/llm_tenant.rb +8 -7
  77. data/lib/ruby_llm/agents/core/version.rb +1 -1
  78. data/lib/ruby_llm/agents/dsl/base.rb +157 -17
  79. data/lib/ruby_llm/agents/dsl/caching.rb +33 -2
  80. data/lib/ruby_llm/agents/dsl/reliability.rb +148 -0
  81. data/lib/ruby_llm/agents/dsl.rb +1 -2
  82. data/lib/ruby_llm/agents/image/analyzer/execution.rb +1 -2
  83. data/lib/ruby_llm/agents/image/background_remover/execution.rb +1 -2
  84. data/lib/ruby_llm/agents/image/concerns/image_operation_dsl.rb +1 -13
  85. data/lib/ruby_llm/agents/image/concerns/image_operation_execution.rb +2 -2
  86. data/lib/ruby_llm/agents/image/editor/dsl.rb +0 -14
  87. data/lib/ruby_llm/agents/image/editor/execution.rb +1 -10
  88. data/lib/ruby_llm/agents/image/editor.rb +0 -1
  89. data/lib/ruby_llm/agents/image/generator.rb +0 -21
  90. data/lib/ruby_llm/agents/image/pipeline/dsl.rb +0 -13
  91. data/lib/ruby_llm/agents/image/pipeline/execution.rb +0 -1
  92. data/lib/ruby_llm/agents/image/transformer/dsl.rb +0 -13
  93. data/lib/ruby_llm/agents/image/transformer/execution.rb +1 -10
  94. data/lib/ruby_llm/agents/image/transformer.rb +0 -1
  95. data/lib/ruby_llm/agents/image/upscaler/execution.rb +1 -2
  96. data/lib/ruby_llm/agents/image/variator/execution.rb +1 -2
  97. data/lib/ruby_llm/agents/infrastructure/alert_manager.rb +78 -173
  98. data/lib/ruby_llm/agents/infrastructure/budget/budget_query.rb +66 -2
  99. data/lib/ruby_llm/agents/infrastructure/budget/spend_recorder.rb +0 -12
  100. data/lib/ruby_llm/agents/infrastructure/circuit_breaker.rb +10 -13
  101. data/lib/ruby_llm/agents/infrastructure/execution_logger_job.rb +8 -0
  102. data/lib/ruby_llm/agents/pipeline/context.rb +0 -1
  103. data/lib/ruby_llm/agents/pipeline/middleware/budget.rb +28 -4
  104. data/lib/ruby_llm/agents/pipeline/middleware/cache.rb +3 -10
  105. data/lib/ruby_llm/agents/pipeline/middleware/instrumentation.rb +88 -55
  106. data/lib/ruby_llm/agents/pipeline/middleware/tenant.rb +5 -41
  107. data/lib/ruby_llm/agents/rails/engine.rb +6 -6
  108. data/lib/ruby_llm/agents/results/base.rb +1 -49
  109. data/lib/ruby_llm/agents/text/embedder.rb +0 -1
  110. data/lib/ruby_llm/agents.rb +1 -9
  111. data/lib/tasks/ruby_llm_agents.rake +34 -0
  112. metadata +14 -83
  113. data/app/controllers/ruby_llm/agents/api_configurations_controller.rb +0 -214
  114. data/app/controllers/ruby_llm/agents/workflows_controller.rb +0 -544
  115. data/app/mailers/ruby_llm/agents/alert_mailer.rb +0 -84
  116. data/app/mailers/ruby_llm/agents/application_mailer.rb +0 -28
  117. data/app/models/ruby_llm/agents/api_configuration.rb +0 -386
  118. data/app/models/ruby_llm/agents/execution/workflow.rb +0 -170
  119. data/app/models/ruby_llm/agents/tenant/configurable.rb +0 -135
  120. data/app/views/ruby_llm/agents/agents/_agent.html.erb +0 -98
  121. data/app/views/ruby_llm/agents/agents/_version_comparison.html.erb +0 -186
  122. data/app/views/ruby_llm/agents/agents/_workflow.html.erb +0 -126
  123. data/app/views/ruby_llm/agents/alert_mailer/alert_notification.html.erb +0 -107
  124. data/app/views/ruby_llm/agents/alert_mailer/alert_notification.text.erb +0 -18
  125. data/app/views/ruby_llm/agents/api_configurations/_api_key_field.html.erb +0 -34
  126. data/app/views/ruby_llm/agents/api_configurations/_form.html.erb +0 -288
  127. data/app/views/ruby_llm/agents/api_configurations/edit.html.erb +0 -95
  128. data/app/views/ruby_llm/agents/api_configurations/edit_tenant.html.erb +0 -97
  129. data/app/views/ruby_llm/agents/api_configurations/show.html.erb +0 -214
  130. data/app/views/ruby_llm/agents/api_configurations/tenant.html.erb +0 -179
  131. data/app/views/ruby_llm/agents/dashboard/_agent_comparison.html.erb +0 -73
  132. data/app/views/ruby_llm/agents/dashboard/_alerts_feed.html.erb +0 -62
  133. data/app/views/ruby_llm/agents/dashboard/_breaker_strip.html.erb +0 -47
  134. data/app/views/ruby_llm/agents/dashboard/_budgets_bar.html.erb +0 -75
  135. data/app/views/ruby_llm/agents/dashboard/_model_comparison.html.erb +0 -56
  136. data/app/views/ruby_llm/agents/dashboard/_model_cost_breakdown.html.erb +0 -115
  137. data/app/views/ruby_llm/agents/dashboard/_now_strip.html.erb +0 -59
  138. data/app/views/ruby_llm/agents/dashboard/_top_errors.html.erb +0 -60
  139. data/app/views/ruby_llm/agents/executions/_workflow_summary.html.erb +0 -86
  140. data/app/views/ruby_llm/agents/executions/dry_run.html.erb +0 -149
  141. data/app/views/ruby_llm/agents/shared/_breadcrumbs.html.erb +0 -48
  142. data/app/views/ruby_llm/agents/shared/_nav_link.html.erb +0 -27
  143. data/app/views/ruby_llm/agents/shared/_stat_card.html.erb +0 -14
  144. data/app/views/ruby_llm/agents/shared/_workflow_type_badge.html.erb +0 -35
  145. data/app/views/ruby_llm/agents/workflows/_empty_state.html.erb +0 -22
  146. data/app/views/ruby_llm/agents/workflows/_step_performance.html.erb +0 -228
  147. data/app/views/ruby_llm/agents/workflows/_structure_dsl.html.erb +0 -539
  148. data/app/views/ruby_llm/agents/workflows/_structure_parallel.html.erb +0 -76
  149. data/app/views/ruby_llm/agents/workflows/_structure_pipeline.html.erb +0 -74
  150. data/app/views/ruby_llm/agents/workflows/_structure_router.html.erb +0 -108
  151. data/app/views/ruby_llm/agents/workflows/_workflow_diagram.html.erb +0 -920
  152. data/app/views/ruby_llm/agents/workflows/index.html.erb +0 -179
  153. data/app/views/ruby_llm/agents/workflows/show.html.erb +0 -467
  154. data/lib/generators/ruby_llm_agents/api_configuration_generator.rb +0 -100
  155. data/lib/generators/ruby_llm_agents/templates/add_workflow_migration.rb.tt +0 -38
  156. data/lib/generators/ruby_llm_agents/templates/application_workflow.rb.tt +0 -48
  157. data/lib/generators/ruby_llm_agents/templates/create_api_configurations_migration.rb.tt +0 -90
  158. data/lib/generators/ruby_llm_agents/templates/skills/WORKFLOWS.md.tt +0 -551
  159. data/lib/ruby_llm/agents/core/base/moderation_dsl.rb +0 -181
  160. data/lib/ruby_llm/agents/core/base/moderation_execution.rb +0 -274
  161. data/lib/ruby_llm/agents/core/resolved_config.rb +0 -348
  162. data/lib/ruby_llm/agents/image/generator/content_policy.rb +0 -95
  163. data/lib/ruby_llm/agents/infrastructure/redactor.rb +0 -130
  164. data/lib/ruby_llm/agents/results/moderation_result.rb +0 -158
  165. data/lib/ruby_llm/agents/text/moderator.rb +0 -237
  166. data/lib/ruby_llm/agents/workflow/approval.rb +0 -205
  167. data/lib/ruby_llm/agents/workflow/approval_store.rb +0 -179
  168. data/lib/ruby_llm/agents/workflow/async.rb +0 -220
  169. data/lib/ruby_llm/agents/workflow/async_executor.rb +0 -156
  170. data/lib/ruby_llm/agents/workflow/dsl/executor.rb +0 -467
  171. data/lib/ruby_llm/agents/workflow/dsl/input_schema.rb +0 -244
  172. data/lib/ruby_llm/agents/workflow/dsl/iteration_executor.rb +0 -289
  173. data/lib/ruby_llm/agents/workflow/dsl/parallel_group.rb +0 -107
  174. data/lib/ruby_llm/agents/workflow/dsl/route_builder.rb +0 -150
  175. data/lib/ruby_llm/agents/workflow/dsl/schedule_helpers.rb +0 -187
  176. data/lib/ruby_llm/agents/workflow/dsl/step_config.rb +0 -352
  177. data/lib/ruby_llm/agents/workflow/dsl/step_executor.rb +0 -415
  178. data/lib/ruby_llm/agents/workflow/dsl/wait_config.rb +0 -257
  179. data/lib/ruby_llm/agents/workflow/dsl/wait_executor.rb +0 -317
  180. data/lib/ruby_llm/agents/workflow/dsl.rb +0 -576
  181. data/lib/ruby_llm/agents/workflow/instrumentation.rb +0 -249
  182. data/lib/ruby_llm/agents/workflow/notifiers/base.rb +0 -117
  183. data/lib/ruby_llm/agents/workflow/notifiers/email.rb +0 -117
  184. data/lib/ruby_llm/agents/workflow/notifiers/slack.rb +0 -180
  185. data/lib/ruby_llm/agents/workflow/notifiers/webhook.rb +0 -121
  186. data/lib/ruby_llm/agents/workflow/notifiers.rb +0 -70
  187. data/lib/ruby_llm/agents/workflow/orchestrator.rb +0 -416
  188. data/lib/ruby_llm/agents/workflow/result.rb +0 -592
  189. data/lib/ruby_llm/agents/workflow/thread_pool.rb +0 -185
  190. data/lib/ruby_llm/agents/workflow/throttle_manager.rb +0 -206
  191. data/lib/ruby_llm/agents/workflow/wait_result.rb +0 -213
@@ -1,244 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module RubyLLM
4
- module Agents
5
- class Workflow
6
- module DSL
7
- # Defines and validates input schema for a workflow
8
- #
9
- # Provides a DSL for declaring required and optional input parameters
10
- # with type validation and default values.
11
- #
12
- # @example Defining input schema
13
- # class MyWorkflow < RubyLLM::Agents::Workflow
14
- # input do
15
- # required :order_id, String
16
- # required :user_id, Integer
17
- # optional :priority, String, default: "normal"
18
- # optional :expedited, Boolean, default: false
19
- # end
20
- # end
21
- #
22
- # @api private
23
- class InputSchema
24
- # Error raised when input validation fails
25
- class ValidationError < StandardError
26
- attr_reader :errors
27
-
28
- def initialize(message, errors: [])
29
- super(message)
30
- @errors = errors
31
- end
32
- end
33
-
34
- # Represents a single field in the schema
35
- class Field
36
- attr_reader :name, :type, :required, :default, :options
37
-
38
- def initialize(name, type, required:, default: nil, **options)
39
- @name = name
40
- @type = type
41
- @required = required
42
- @default = default
43
- @options = options
44
- end
45
-
46
- def required?
47
- @required
48
- end
49
-
50
- def optional?
51
- !@required
52
- end
53
-
54
- def has_default?
55
- !@default.nil? || @options.key?(:default)
56
- end
57
-
58
- def validate(value)
59
- errors = []
60
-
61
- # Check required
62
- if required? && value.nil?
63
- errors << "#{name} is required"
64
- return errors
65
- end
66
-
67
- # Skip validation for nil optional values
68
- return errors if value.nil? && optional?
69
-
70
- # Type validation
71
- unless valid_type?(value)
72
- errors << "#{name} must be a #{type_description}"
73
- end
74
-
75
- # Enum validation
76
- if options[:in] && !options[:in].include?(value)
77
- errors << "#{name} must be one of: #{options[:in].join(', ')}"
78
- end
79
-
80
- # Custom validation
81
- if options[:validate] && !options[:validate].call(value)
82
- errors << "#{name} failed custom validation"
83
- end
84
-
85
- errors
86
- end
87
-
88
- def to_h
89
- {
90
- name: name,
91
- type: type_description,
92
- required: required?,
93
- default: default,
94
- options: options.except(:validate)
95
- }.compact
96
- end
97
-
98
- private
99
-
100
- def valid_type?(value)
101
- return true if type.nil?
102
-
103
- case type
104
- when :boolean, "Boolean"
105
- value == true || value == false
106
- else
107
- value.is_a?(type)
108
- end
109
- end
110
-
111
- def type_description
112
- case type
113
- when :boolean, "Boolean"
114
- "Boolean"
115
- when Class
116
- type.name
117
- else
118
- type.to_s
119
- end
120
- end
121
- end
122
-
123
- def initialize
124
- @fields = {}
125
- end
126
-
127
- # Defines a required field
128
- #
129
- # @param name [Symbol] Field name
130
- # @param type [Class, Symbol] Expected type
131
- # @param options [Hash] Additional options
132
- # @return [void]
133
- def required(name, type = nil, **options)
134
- @fields[name] = Field.new(name, type, required: true, **options)
135
- end
136
-
137
- # Defines an optional field
138
- #
139
- # @param name [Symbol] Field name
140
- # @param type [Class, Symbol] Expected type
141
- # @param default [Object] Default value
142
- # @param options [Hash] Additional options
143
- # @return [void]
144
- def optional(name, type = nil, default: nil, **options)
145
- @fields[name] = Field.new(name, type, required: false, default: default, **options)
146
- end
147
-
148
- # Returns all fields
149
- #
150
- # @return [Hash<Symbol, Field>]
151
- attr_reader :fields
152
-
153
- # Returns required field names
154
- #
155
- # @return [Array<Symbol>]
156
- def required_fields
157
- @fields.select { |_, f| f.required? }.keys
158
- end
159
-
160
- # Returns optional field names
161
- #
162
- # @return [Array<Symbol>]
163
- def optional_fields
164
- @fields.select { |_, f| f.optional? }.keys
165
- end
166
-
167
- # Validates input against the schema
168
- #
169
- # @param input [Hash] Input data to validate
170
- # @return [Hash] Validated and normalized input
171
- # @raise [ValidationError] If validation fails
172
- def validate!(input)
173
- errors = []
174
- normalized = {}
175
-
176
- @fields.each do |name, field|
177
- value = input.key?(name) ? input[name] : field.default
178
- field_errors = field.validate(value)
179
- errors.concat(field_errors)
180
- normalized[name] = value unless value.nil? && field.optional?
181
- end
182
-
183
- # Include any extra fields not in schema
184
- input.each do |key, value|
185
- normalized[key] = value unless @fields.key?(key)
186
- end
187
-
188
- if errors.any?
189
- raise ValidationError.new(
190
- "Input validation failed: #{errors.join(', ')}",
191
- errors: errors
192
- )
193
- end
194
-
195
- normalized
196
- end
197
-
198
- # Applies defaults to input without validation
199
- #
200
- # @param input [Hash] Input data
201
- # @return [Hash] Input with defaults applied
202
- def apply_defaults(input)
203
- result = input.dup
204
- @fields.each do |name, field|
205
- result[name] = field.default if !result.key?(name) && field.has_default?
206
- end
207
- result
208
- end
209
-
210
- # Converts to hash for serialization
211
- #
212
- # @return [Hash]
213
- def to_h
214
- {
215
- fields: @fields.transform_values(&:to_h)
216
- }
217
- end
218
-
219
- # Returns whether the schema is empty
220
- #
221
- # @return [Boolean]
222
- def empty?
223
- @fields.empty?
224
- end
225
- end
226
-
227
- # Output schema for workflow results
228
- #
229
- # Similar to InputSchema but for validating workflow output.
230
- class OutputSchema < InputSchema
231
- # Validates output against the schema
232
- #
233
- # @param output [Hash] Output data to validate
234
- # @return [Hash] Validated output
235
- # @raise [ValidationError] If validation fails
236
- def validate!(output)
237
- output_hash = output.is_a?(Hash) ? output : { result: output }
238
- super(output_hash)
239
- end
240
- end
241
- end
242
- end
243
- end
244
- end
@@ -1,289 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module RubyLLM
4
- module Agents
5
- class Workflow
6
- module DSL
7
- # Executes iteration steps with sequential or parallel processing
8
- #
9
- # Handles `each:` option on steps to process collections with support for:
10
- # - Sequential iteration
11
- # - Parallel iteration with configurable concurrency
12
- # - Fail-fast behavior
13
- # - Continue-on-error behavior
14
- #
15
- # @api private
16
- class IterationExecutor
17
- attr_reader :workflow, :config, :previous_result
18
-
19
- # @param workflow [Workflow] The workflow instance
20
- # @param config [StepConfig] The step configuration
21
- # @param previous_result [Result, nil] Previous step result
22
- def initialize(workflow, config, previous_result)
23
- @workflow = workflow
24
- @config = config
25
- @previous_result = previous_result
26
- end
27
-
28
- # Executes the iteration
29
- #
30
- # @yield [chunk] Streaming callback
31
- # @return [IterationResult] Aggregated results for all items
32
- def execute(&block)
33
- items = resolve_items
34
- return Workflow::IterationResult.empty(config.name) if items.empty?
35
-
36
- if config.iteration_concurrency && config.iteration_concurrency > 1
37
- execute_parallel(items, &block)
38
- else
39
- execute_sequential(items, &block)
40
- end
41
- end
42
-
43
- private
44
-
45
- def resolve_items
46
- source = config.each_source
47
- items = workflow.instance_exec(&source)
48
- Array(items)
49
- rescue StandardError => e
50
- raise IterationSourceError, "Failed to resolve iteration source: #{e.message}"
51
- end
52
-
53
- def execute_sequential(items, &block)
54
- item_results = []
55
- errors = {}
56
-
57
- items.each_with_index do |item, index|
58
- begin
59
- result = execute_for_item(item, index, &block)
60
- item_results << result
61
-
62
- # Check for fail-fast on error
63
- if config.iteration_fail_fast? && result.respond_to?(:error?) && result.error?
64
- break
65
- end
66
- rescue StandardError => e
67
- if config.iteration_fail_fast?
68
- errors[index] = e
69
- break
70
- elsif config.continue_on_error?
71
- errors[index] = e
72
- # Continue to next item
73
- else
74
- raise
75
- end
76
- end
77
- end
78
-
79
- Workflow::IterationResult.new(
80
- step_name: config.name,
81
- item_results: item_results,
82
- errors: errors
83
- )
84
- end
85
-
86
- def execute_parallel(items, &block)
87
- results_mutex = Mutex.new
88
- item_results = Array.new(items.size)
89
- errors = {}
90
- aborted = false
91
-
92
- pool = create_executor_pool(config.iteration_concurrency)
93
-
94
- items.each_with_index do |item, index|
95
- pool.post do
96
- next if aborted
97
-
98
- begin
99
- result = execute_for_item(item, index, &block)
100
-
101
- results_mutex.synchronize do
102
- item_results[index] = result
103
-
104
- # Check for fail-fast
105
- if config.iteration_fail_fast? && result.respond_to?(:error?) && result.error?
106
- aborted = true
107
- pool.abort! if pool.respond_to?(:abort!)
108
- end
109
- end
110
- rescue StandardError => e
111
- results_mutex.synchronize do
112
- errors[index] = e
113
-
114
- if config.iteration_fail_fast?
115
- aborted = true
116
- pool.abort! if pool.respond_to?(:abort!)
117
- end
118
- end
119
-
120
- raise unless config.continue_on_error? || config.iteration_fail_fast?
121
- end
122
- end
123
- end
124
-
125
- pool.wait_for_completion
126
- pool.shutdown
127
-
128
- # Remove nil entries from results (unfilled due to abort)
129
- item_results.compact!
130
-
131
- Workflow::IterationResult.new(
132
- step_name: config.name,
133
- item_results: item_results,
134
- errors: errors
135
- )
136
- end
137
-
138
- def execute_for_item(item, index, &block)
139
- if config.custom_block?
140
- execute_block_for_item(item, index, &block)
141
- elsif config.workflow?
142
- execute_workflow_for_item(item, index, &block)
143
- else
144
- execute_agent_for_item(item, index, &block)
145
- end
146
- end
147
-
148
- def execute_block_for_item(item, index, &block)
149
- context = IterationContext.new(workflow, config, previous_result, item, index)
150
- result = context.instance_exec(item, &config.block)
151
-
152
- # If block returns a Result, use it; otherwise wrap it
153
- if result.is_a?(Workflow::Result) || result.is_a?(RubyLLM::Agents::Result)
154
- result
155
- else
156
- SimpleResult.new(content: result, success: true)
157
- end
158
- end
159
-
160
- def execute_agent_for_item(item, index, &block)
161
- # Build input for this item
162
- step_input = build_item_input(item, index)
163
- workflow.send(:execute_agent, config.agent, step_input, step_name: config.name, &block)
164
- end
165
-
166
- def execute_workflow_for_item(item, index, &block)
167
- step_input = build_item_input(item, index)
168
-
169
- # Build execution metadata
170
- parent_metadata = {
171
- parent_execution_id: workflow.execution_id,
172
- root_execution_id: workflow.send(:root_execution_id),
173
- workflow_id: workflow.workflow_id,
174
- workflow_type: workflow.class.name,
175
- workflow_step: config.name.to_s,
176
- iteration_index: index,
177
- recursion_depth: (workflow.instance_variable_get(:@recursion_depth) || 0) + (config.agent == workflow.class ? 1 : 0)
178
- }.compact
179
-
180
- merged_input = step_input.merge(
181
- execution_metadata: parent_metadata.merge(step_input[:execution_metadata] || {})
182
- )
183
-
184
- result = config.agent.call(**merged_input, &block)
185
-
186
- # Track accumulated cost
187
- if result.respond_to?(:total_cost) && result.total_cost
188
- workflow.instance_variable_set(
189
- :@accumulated_cost,
190
- (workflow.instance_variable_get(:@accumulated_cost) || 0.0) + result.total_cost
191
- )
192
- workflow.send(:check_cost_threshold!)
193
- end
194
-
195
- Workflow::SubWorkflowResult.new(
196
- content: result.content,
197
- sub_workflow_result: result,
198
- workflow_type: config.agent.name,
199
- step_name: config.name
200
- )
201
- end
202
-
203
- def build_item_input(item, index)
204
- # If there's an input mapper, use it with item context
205
- if config.input_mapper
206
- # Create a temporary context that has access to item and index
207
- context = IterationInputContext.new(workflow, item, index)
208
- context.instance_exec(&config.input_mapper)
209
- else
210
- # Default: wrap item in a hash
211
- item.is_a?(Hash) ? item : { item: item, index: index }
212
- end
213
- end
214
-
215
- def create_executor_pool(size)
216
- config_obj = RubyLLM::Agents.configuration
217
-
218
- if config_obj.respond_to?(:async_context?) && config_obj.async_context?
219
- AsyncExecutor.new(max_concurrent: size)
220
- else
221
- ThreadPool.new(size: size)
222
- end
223
- end
224
- end
225
-
226
- # Context for executing iteration block steps
227
- #
228
- # Extends BlockContext with item and index access.
229
- #
230
- # @api private
231
- class IterationContext < BlockContext
232
- attr_reader :item, :index
233
-
234
- def initialize(workflow, config, previous_result, item, index)
235
- super(workflow, config, previous_result)
236
- @item = item
237
- @index = index
238
- end
239
-
240
- # Access the current item being processed
241
- def current_item
242
- @item
243
- end
244
-
245
- # Access the current iteration index
246
- def current_index
247
- @index
248
- end
249
- end
250
-
251
- # Context for building iteration input
252
- #
253
- # Provides access to item and index for input mappers.
254
- #
255
- # @api private
256
- class IterationInputContext
257
- def initialize(workflow, item, index)
258
- @workflow = workflow
259
- @item = item
260
- @index = index
261
- end
262
-
263
- attr_reader :item, :index
264
-
265
- # Access workflow input
266
- def input
267
- @workflow.input
268
- end
269
-
270
- # Delegate to workflow for step results access
271
- def method_missing(name, *args, &block)
272
- if @workflow.respond_to?(name, true)
273
- @workflow.send(name, *args, &block)
274
- else
275
- super
276
- end
277
- end
278
-
279
- def respond_to_missing?(name, include_private = false)
280
- @workflow.respond_to?(name, include_private) || super
281
- end
282
- end
283
-
284
- # Error raised when iteration source resolution fails
285
- class IterationSourceError < StandardError; end
286
- end
287
- end
288
- end
289
- end
@@ -1,107 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module RubyLLM
4
- module Agents
5
- class Workflow
6
- module DSL
7
- # Represents a group of steps that execute in parallel
8
- #
9
- # Parallel groups allow multiple steps to run concurrently and
10
- # their results to be available to subsequent steps.
11
- #
12
- # @example Basic parallel group
13
- # parallel do
14
- # step :sentiment, SentimentAgent
15
- # step :keywords, KeywordAgent
16
- # step :entities, EntityAgent
17
- # end
18
- #
19
- # @example Named parallel group
20
- # parallel :analysis do
21
- # step :sentiment, SentimentAgent
22
- # step :keywords, KeywordAgent
23
- # end
24
- #
25
- # step :combine, CombinerAgent,
26
- # input: -> { { analysis: analysis } }
27
- #
28
- # @api private
29
- class ParallelGroup
30
- attr_reader :name, :step_names, :options
31
-
32
- # @param name [Symbol, nil] Optional name for the group
33
- # @param step_names [Array<Symbol>] Names of steps in the group
34
- # @param options [Hash] Group options
35
- def initialize(name: nil, step_names: [], options: {})
36
- @name = name
37
- @step_names = step_names
38
- @options = options
39
- end
40
-
41
- # Adds a step to the group
42
- #
43
- # @param step_name [Symbol]
44
- # @return [void]
45
- def add_step(step_name)
46
- @step_names << step_name
47
- end
48
-
49
- # Returns the number of steps in the group
50
- #
51
- # @return [Integer]
52
- def size
53
- @step_names.size
54
- end
55
-
56
- # Returns whether the group is empty
57
- #
58
- # @return [Boolean]
59
- def empty?
60
- @step_names.empty?
61
- end
62
-
63
- # Returns the fail-fast setting for this group
64
- #
65
- # @return [Boolean]
66
- def fail_fast?
67
- options[:fail_fast] == true
68
- end
69
-
70
- # Returns the concurrency limit for this group
71
- #
72
- # @return [Integer, nil]
73
- def concurrency
74
- options[:concurrency]
75
- end
76
-
77
- # Returns the timeout for the entire group
78
- #
79
- # @return [Integer, nil]
80
- def timeout
81
- options[:timeout]
82
- end
83
-
84
- # Converts to hash for serialization
85
- #
86
- # @return [Hash]
87
- def to_h
88
- {
89
- name: name,
90
- step_names: step_names,
91
- fail_fast: fail_fast?,
92
- concurrency: concurrency,
93
- timeout: timeout
94
- }.compact
95
- end
96
-
97
- # String representation
98
- #
99
- # @return [String]
100
- def inspect
101
- "#<ParallelGroup name=#{name.inspect} steps=#{step_names.inspect}>"
102
- end
103
- end
104
- end
105
- end
106
- end
107
- end