ruby_llm-agents 1.0.0 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/app/controllers/concerns/ruby_llm/agents/paginatable.rb +9 -3
- data/app/controllers/concerns/ruby_llm/agents/sortable.rb +58 -0
- data/app/controllers/ruby_llm/agents/agents_controller.rb +59 -16
- data/app/controllers/ruby_llm/agents/dashboard_controller.rb +144 -20
- data/app/controllers/ruby_llm/agents/executions_controller.rb +13 -16
- data/app/controllers/ruby_llm/agents/workflows_controller.rb +279 -90
- data/app/helpers/ruby_llm/agents/application_helper.rb +100 -0
- data/app/mailers/ruby_llm/agents/alert_mailer.rb +84 -0
- data/app/mailers/ruby_llm/agents/application_mailer.rb +28 -0
- data/app/models/ruby_llm/agents/execution/analytics.rb +170 -20
- data/app/models/ruby_llm/agents/execution/scopes.rb +0 -31
- data/app/models/ruby_llm/agents/execution/workflow.rb +0 -129
- data/app/models/ruby_llm/agents/execution.rb +50 -14
- data/app/models/ruby_llm/agents/tenant/budgetable.rb +277 -0
- data/app/models/ruby_llm/agents/tenant/configurable.rb +135 -0
- data/app/models/ruby_llm/agents/tenant/trackable.rb +310 -0
- data/app/models/ruby_llm/agents/tenant.rb +146 -0
- data/app/models/ruby_llm/agents/tenant_budget.rb +12 -253
- data/app/services/ruby_llm/agents/agent_registry.rb +18 -12
- data/app/views/layouts/ruby_llm/agents/application.html.erb +72 -76
- data/app/views/ruby_llm/agents/agents/_agent.html.erb +0 -12
- data/app/views/ruby_llm/agents/agents/_sortable_header.html.erb +56 -0
- data/app/views/ruby_llm/agents/agents/_workflow.html.erb +5 -15
- data/app/views/ruby_llm/agents/agents/index.html.erb +271 -100
- data/app/views/ruby_llm/agents/agents/show.html.erb +1 -0
- data/app/views/ruby_llm/agents/alert_mailer/alert_notification.html.erb +107 -0
- data/app/views/ruby_llm/agents/alert_mailer/alert_notification.text.erb +18 -0
- data/app/views/ruby_llm/agents/api_configurations/show.html.erb +4 -1
- data/app/views/ruby_llm/agents/dashboard/_agent_comparison.html.erb +66 -359
- data/app/views/ruby_llm/agents/dashboard/_model_comparison.html.erb +56 -0
- data/app/views/ruby_llm/agents/dashboard/_model_cost_breakdown.html.erb +115 -0
- data/app/views/ruby_llm/agents/dashboard/_now_strip.html.erb +35 -60
- data/app/views/ruby_llm/agents/dashboard/_top_errors.html.erb +17 -6
- data/app/views/ruby_llm/agents/dashboard/index.html.erb +373 -72
- data/app/views/ruby_llm/agents/executions/_execution.html.erb +0 -1
- data/app/views/ruby_llm/agents/executions/_filters.html.erb +51 -39
- data/app/views/ruby_llm/agents/executions/_list.html.erb +53 -195
- data/app/views/ruby_llm/agents/executions/_workflow_summary.html.erb +5 -20
- data/app/views/ruby_llm/agents/executions/index.html.erb +7 -83
- data/app/views/ruby_llm/agents/executions/show.html.erb +10 -20
- data/app/views/ruby_llm/agents/shared/_agent_type_badge.html.erb +2 -1
- data/app/views/ruby_llm/agents/shared/_doc_link.html.erb +12 -0
- data/app/views/ruby_llm/agents/shared/_executions_table.html.erb +3 -15
- data/app/views/ruby_llm/agents/shared/_filter_dropdown.html.erb +1 -1
- data/app/views/ruby_llm/agents/shared/_select_dropdown.html.erb +1 -1
- data/app/views/ruby_llm/agents/shared/_sortable_header.html.erb +53 -0
- data/app/views/ruby_llm/agents/shared/_status_badge.html.erb +7 -0
- data/app/views/ruby_llm/agents/shared/_status_dot.html.erb +1 -1
- data/app/views/ruby_llm/agents/shared/_workflow_type_badge.html.erb +9 -35
- data/app/views/ruby_llm/agents/system_config/show.html.erb +4 -1
- data/app/views/ruby_llm/agents/tenants/index.html.erb +4 -1
- data/app/views/ruby_llm/agents/workflows/_step_performance.html.erb +7 -15
- data/app/views/ruby_llm/agents/workflows/_structure_dsl.html.erb +539 -0
- data/app/views/ruby_llm/agents/workflows/_workflow_diagram.html.erb +920 -0
- data/app/views/ruby_llm/agents/workflows/index.html.erb +179 -0
- data/app/views/ruby_llm/agents/workflows/show.html.erb +164 -139
- data/config/routes.rb +1 -1
- data/lib/generators/ruby_llm_agents/agent_generator.rb +6 -36
- data/lib/generators/ruby_llm_agents/background_remover_generator.rb +7 -37
- data/lib/generators/ruby_llm_agents/embedder_generator.rb +5 -38
- data/lib/generators/ruby_llm_agents/image_analyzer_generator.rb +7 -37
- data/lib/generators/ruby_llm_agents/image_editor_generator.rb +7 -37
- data/lib/generators/ruby_llm_agents/image_generator_generator.rb +8 -41
- data/lib/generators/ruby_llm_agents/image_pipeline_generator.rb +18 -46
- data/lib/generators/ruby_llm_agents/image_transformer_generator.rb +7 -37
- data/lib/generators/ruby_llm_agents/image_upscaler_generator.rb +7 -37
- data/lib/generators/ruby_llm_agents/image_variator_generator.rb +7 -37
- data/lib/generators/ruby_llm_agents/install_generator.rb +33 -56
- data/lib/generators/ruby_llm_agents/migrate_structure_generator.rb +480 -0
- data/lib/generators/ruby_llm_agents/multi_tenancy_generator.rb +42 -22
- data/lib/generators/ruby_llm_agents/restructure_generator.rb +2 -2
- data/lib/generators/ruby_llm_agents/speaker_generator.rb +8 -39
- data/lib/generators/ruby_llm_agents/templates/add_tenant_to_executions_migration.rb.tt +13 -2
- data/lib/generators/ruby_llm_agents/templates/agent.rb.tt +5 -8
- data/lib/generators/ruby_llm_agents/templates/application_agent.rb.tt +40 -42
- data/lib/generators/ruby_llm_agents/templates/application_background_remover.rb.tt +20 -22
- data/lib/generators/ruby_llm_agents/templates/application_embedder.rb.tt +24 -26
- data/lib/generators/ruby_llm_agents/templates/application_image_analyzer.rb.tt +20 -22
- data/lib/generators/ruby_llm_agents/templates/application_image_editor.rb.tt +19 -17
- data/lib/generators/ruby_llm_agents/templates/application_image_generator.rb.tt +31 -33
- data/lib/generators/ruby_llm_agents/templates/application_image_pipeline.rb.tt +125 -127
- data/lib/generators/ruby_llm_agents/templates/application_image_transformer.rb.tt +20 -18
- data/lib/generators/ruby_llm_agents/templates/application_image_upscaler.rb.tt +19 -17
- data/lib/generators/ruby_llm_agents/templates/application_image_variator.rb.tt +19 -17
- data/lib/generators/ruby_llm_agents/templates/application_speaker.rb.tt +38 -40
- data/lib/generators/ruby_llm_agents/templates/application_transcriber.rb.tt +42 -44
- data/lib/generators/ruby_llm_agents/templates/application_workflow.rb.tt +48 -0
- data/lib/generators/ruby_llm_agents/templates/background_remover.rb.tt +19 -21
- data/lib/generators/ruby_llm_agents/templates/create_tenant_budgets_migration.rb.tt +11 -0
- data/lib/generators/ruby_llm_agents/templates/create_tenants_migration.rb.tt +72 -0
- data/lib/generators/ruby_llm_agents/templates/embedder.rb.tt +19 -21
- data/lib/generators/ruby_llm_agents/templates/image_analyzer.rb.tt +20 -22
- data/lib/generators/ruby_llm_agents/templates/image_editor.rb.tt +15 -17
- data/lib/generators/ruby_llm_agents/templates/image_generator.rb.tt +25 -27
- data/lib/generators/ruby_llm_agents/templates/image_pipeline.rb.tt +19 -21
- data/lib/generators/ruby_llm_agents/templates/image_transformer.rb.tt +20 -22
- data/lib/generators/ruby_llm_agents/templates/image_upscaler.rb.tt +17 -19
- data/lib/generators/ruby_llm_agents/templates/image_variator.rb.tt +15 -17
- data/lib/generators/ruby_llm_agents/templates/rename_tenant_budgets_to_tenants_migration.rb.tt +34 -0
- data/lib/generators/ruby_llm_agents/templates/skills/AGENTS.md.tt +87 -24
- data/lib/generators/ruby_llm_agents/templates/skills/BACKGROUND_REMOVERS.md.tt +21 -27
- data/lib/generators/ruby_llm_agents/templates/skills/EMBEDDERS.md.tt +46 -54
- data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_ANALYZERS.md.tt +31 -39
- data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_EDITORS.md.tt +22 -28
- data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_GENERATORS.md.tt +53 -63
- data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_PIPELINES.md.tt +46 -56
- data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_TRANSFORMERS.md.tt +23 -31
- data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_UPSCALERS.md.tt +22 -30
- data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_VARIATORS.md.tt +23 -31
- data/lib/generators/ruby_llm_agents/templates/skills/SPEAKERS.md.tt +38 -46
- data/lib/generators/ruby_llm_agents/templates/skills/TOOLS.md.tt +7 -7
- data/lib/generators/ruby_llm_agents/templates/skills/TRANSCRIBERS.md.tt +59 -71
- data/lib/generators/ruby_llm_agents/templates/skills/WORKFLOWS.md.tt +274 -23
- data/lib/generators/ruby_llm_agents/templates/speaker.rb.tt +29 -31
- data/lib/generators/ruby_llm_agents/templates/transcriber.rb.tt +28 -30
- data/lib/generators/ruby_llm_agents/transcriber_generator.rb +10 -43
- data/lib/generators/ruby_llm_agents/upgrade_generator.rb +26 -0
- data/lib/ruby_llm/agents/core/configuration.rb +55 -43
- data/lib/ruby_llm/agents/core/llm_tenant.rb +60 -60
- data/lib/ruby_llm/agents/core/version.rb +1 -1
- data/lib/ruby_llm/agents/infrastructure/alert_manager.rb +26 -0
- data/lib/ruby_llm/agents/infrastructure/budget/config_resolver.rb +4 -2
- data/lib/ruby_llm/agents/pipeline.rb +69 -0
- data/lib/ruby_llm/agents/workflow/approval.rb +205 -0
- data/lib/ruby_llm/agents/workflow/approval_store.rb +179 -0
- data/lib/ruby_llm/agents/workflow/dsl/executor.rb +467 -0
- data/lib/ruby_llm/agents/workflow/dsl/input_schema.rb +244 -0
- data/lib/ruby_llm/agents/workflow/dsl/iteration_executor.rb +289 -0
- data/lib/ruby_llm/agents/workflow/dsl/parallel_group.rb +107 -0
- data/lib/ruby_llm/agents/workflow/dsl/route_builder.rb +150 -0
- data/lib/ruby_llm/agents/workflow/dsl/schedule_helpers.rb +187 -0
- data/lib/ruby_llm/agents/workflow/dsl/step_config.rb +352 -0
- data/lib/ruby_llm/agents/workflow/dsl/step_executor.rb +415 -0
- data/lib/ruby_llm/agents/workflow/dsl/wait_config.rb +257 -0
- data/lib/ruby_llm/agents/workflow/dsl/wait_executor.rb +317 -0
- data/lib/ruby_llm/agents/workflow/dsl.rb +576 -0
- data/lib/ruby_llm/agents/workflow/instrumentation.rb +2 -7
- data/lib/ruby_llm/agents/workflow/notifiers/base.rb +117 -0
- data/lib/ruby_llm/agents/workflow/notifiers/email.rb +117 -0
- data/lib/ruby_llm/agents/workflow/notifiers/slack.rb +180 -0
- data/lib/ruby_llm/agents/workflow/notifiers/webhook.rb +121 -0
- data/lib/ruby_llm/agents/workflow/notifiers.rb +70 -0
- data/lib/ruby_llm/agents/workflow/orchestrator.rb +190 -23
- data/lib/ruby_llm/agents/workflow/result.rb +202 -0
- data/lib/ruby_llm/agents/workflow/throttle_manager.rb +206 -0
- data/lib/ruby_llm/agents/workflow/wait_result.rb +213 -0
- metadata +43 -6
- data/app/views/ruby_llm/agents/dashboard/_execution_item.html.erb +0 -66
- data/lib/ruby_llm/agents/workflow/parallel.rb +0 -299
- data/lib/ruby_llm/agents/workflow/pipeline.rb +0 -306
- data/lib/ruby_llm/agents/workflow/router.rb +0 -429
|
@@ -1,299 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module RubyLLM
|
|
4
|
-
module Agents
|
|
5
|
-
class Workflow
|
|
6
|
-
# Concurrent workflow execution pattern
|
|
7
|
-
#
|
|
8
|
-
# Executes multiple agents simultaneously and aggregates their results.
|
|
9
|
-
# Supports fail-fast behavior, optional branches, and custom aggregation.
|
|
10
|
-
#
|
|
11
|
-
# @example Basic parallel execution
|
|
12
|
-
# class ReviewAnalyzer < RubyLLM::Agents::Workflow::Parallel
|
|
13
|
-
# version "1.0"
|
|
14
|
-
#
|
|
15
|
-
# branch :sentiment, agent: SentimentAgent
|
|
16
|
-
# branch :summary, agent: SummaryAgent
|
|
17
|
-
# branch :categories, agent: CategoryAgent
|
|
18
|
-
# end
|
|
19
|
-
#
|
|
20
|
-
# result = ReviewAnalyzer.call(text: "Great product!")
|
|
21
|
-
# result.branches[:sentiment].content # "positive"
|
|
22
|
-
# result.branches[:summary].content # "User liked the product"
|
|
23
|
-
#
|
|
24
|
-
# @example With optional branches and custom aggregation
|
|
25
|
-
# class FullAnalyzer < RubyLLM::Agents::Workflow::Parallel
|
|
26
|
-
# branch :sentiment, agent: SentimentAgent
|
|
27
|
-
# branch :toxicity, agent: ToxicityAgent, optional: true
|
|
28
|
-
#
|
|
29
|
-
# def aggregate(results)
|
|
30
|
-
# {
|
|
31
|
-
# sentiment: results[:sentiment]&.content,
|
|
32
|
-
# toxicity: results[:toxicity]&.content,
|
|
33
|
-
# safe: results[:toxicity]&.content != "toxic"
|
|
34
|
-
# }
|
|
35
|
-
# end
|
|
36
|
-
# end
|
|
37
|
-
#
|
|
38
|
-
# @example With fail-fast enabled
|
|
39
|
-
# class CriticalAnalyzer < RubyLLM::Agents::Workflow::Parallel
|
|
40
|
-
# fail_fast true # Stop all branches on first failure
|
|
41
|
-
#
|
|
42
|
-
# branch :auth, agent: AuthValidator
|
|
43
|
-
# branch :sanity, agent: SanityChecker
|
|
44
|
-
# end
|
|
45
|
-
#
|
|
46
|
-
# @api public
|
|
47
|
-
class Parallel < Workflow
|
|
48
|
-
class << self
|
|
49
|
-
# Returns the defined branches
|
|
50
|
-
#
|
|
51
|
-
# @return [Hash<Symbol, Hash>] Branch configurations
|
|
52
|
-
def branches
|
|
53
|
-
@branches ||= {}
|
|
54
|
-
end
|
|
55
|
-
|
|
56
|
-
# Inherits branches from parent class
|
|
57
|
-
def inherited(subclass)
|
|
58
|
-
super
|
|
59
|
-
subclass.instance_variable_set(:@branches, branches.dup)
|
|
60
|
-
subclass.instance_variable_set(:@fail_fast, @fail_fast)
|
|
61
|
-
subclass.instance_variable_set(:@concurrency, @concurrency)
|
|
62
|
-
end
|
|
63
|
-
|
|
64
|
-
# Defines a parallel branch
|
|
65
|
-
#
|
|
66
|
-
# @param name [Symbol] Branch identifier
|
|
67
|
-
# @param agent [Class] The agent class to execute
|
|
68
|
-
# @param optional [Boolean] If true, branch failure won't fail the workflow
|
|
69
|
-
# @param input [Proc, nil] Lambda to transform input for this branch
|
|
70
|
-
# @return [void]
|
|
71
|
-
#
|
|
72
|
-
# @example Basic branch
|
|
73
|
-
# branch :analyze, agent: AnalyzerAgent
|
|
74
|
-
#
|
|
75
|
-
# @example Optional branch
|
|
76
|
-
# branch :enrich, agent: EnricherAgent, optional: true
|
|
77
|
-
#
|
|
78
|
-
# @example With custom input
|
|
79
|
-
# branch :summarize, agent: SummaryAgent, input: ->(opts) { { text: opts[:content] } }
|
|
80
|
-
def branch(name, agent:, optional: false, input: nil)
|
|
81
|
-
branches[name] = {
|
|
82
|
-
agent: agent,
|
|
83
|
-
optional: optional,
|
|
84
|
-
input: input
|
|
85
|
-
}
|
|
86
|
-
end
|
|
87
|
-
|
|
88
|
-
# Sets or returns fail-fast behavior
|
|
89
|
-
#
|
|
90
|
-
# When true, cancels remaining branches when any required branch fails.
|
|
91
|
-
#
|
|
92
|
-
# @param value [Boolean, nil] Whether to fail fast
|
|
93
|
-
# @return [Boolean] Current fail-fast setting
|
|
94
|
-
def fail_fast(value = nil)
|
|
95
|
-
if value.nil?
|
|
96
|
-
@fail_fast || false
|
|
97
|
-
else
|
|
98
|
-
@fail_fast = value
|
|
99
|
-
end
|
|
100
|
-
end
|
|
101
|
-
|
|
102
|
-
# Alias for checking fail_fast setting
|
|
103
|
-
def fail_fast?
|
|
104
|
-
fail_fast
|
|
105
|
-
end
|
|
106
|
-
|
|
107
|
-
# Sets or returns concurrency limit
|
|
108
|
-
#
|
|
109
|
-
# @param value [Integer, nil] Max concurrent branches (nil = unlimited)
|
|
110
|
-
# @return [Integer, nil] Current concurrency limit
|
|
111
|
-
def concurrency(value = nil)
|
|
112
|
-
if value.nil?
|
|
113
|
-
@concurrency
|
|
114
|
-
else
|
|
115
|
-
@concurrency = value
|
|
116
|
-
end
|
|
117
|
-
end
|
|
118
|
-
end
|
|
119
|
-
|
|
120
|
-
# Executes the parallel workflow
|
|
121
|
-
#
|
|
122
|
-
# Runs all branches concurrently and aggregates results.
|
|
123
|
-
#
|
|
124
|
-
# @yield [chunk] Yields chunks when streaming (not typically used in parallel)
|
|
125
|
-
# @return [WorkflowResult] The parallel result
|
|
126
|
-
def call(&block)
|
|
127
|
-
instrument_workflow do
|
|
128
|
-
execute_parallel(&block)
|
|
129
|
-
end
|
|
130
|
-
end
|
|
131
|
-
|
|
132
|
-
# Aggregates branch results into final content
|
|
133
|
-
#
|
|
134
|
-
# Override this method to customize how results are combined.
|
|
135
|
-
#
|
|
136
|
-
# @param results [Hash<Symbol, Result>] Branch results
|
|
137
|
-
# @return [Object] Aggregated content
|
|
138
|
-
def aggregate(results)
|
|
139
|
-
# Default: return hash of branch contents
|
|
140
|
-
results.transform_values { |r| r&.content }
|
|
141
|
-
end
|
|
142
|
-
|
|
143
|
-
private
|
|
144
|
-
|
|
145
|
-
# Executes all branches in parallel
|
|
146
|
-
#
|
|
147
|
-
# @return [WorkflowResult] The parallel result
|
|
148
|
-
def execute_parallel(&block)
|
|
149
|
-
results = {}
|
|
150
|
-
errors = {}
|
|
151
|
-
mutex = Mutex.new
|
|
152
|
-
|
|
153
|
-
# Determine pool size based on concurrency setting
|
|
154
|
-
pool_size = self.class.concurrency || self.class.branches.size
|
|
155
|
-
|
|
156
|
-
# Use async executor when in async context, otherwise use thread pool
|
|
157
|
-
pool = create_executor(pool_size)
|
|
158
|
-
|
|
159
|
-
# Submit all branches to the pool
|
|
160
|
-
self.class.branches.each do |name, config|
|
|
161
|
-
pool.post do
|
|
162
|
-
Thread.current.name = "parallel-#{name}"
|
|
163
|
-
Thread.current[:branch_name] = name
|
|
164
|
-
|
|
165
|
-
begin
|
|
166
|
-
# Check if pool was aborted (fail-fast triggered)
|
|
167
|
-
if pool.aborted?
|
|
168
|
-
mutex.synchronize { results[name] = nil }
|
|
169
|
-
next
|
|
170
|
-
end
|
|
171
|
-
|
|
172
|
-
# Build input for this branch
|
|
173
|
-
branch_input = build_branch_input(name, config)
|
|
174
|
-
|
|
175
|
-
# Execute the branch
|
|
176
|
-
result = execute_agent(
|
|
177
|
-
config[:agent],
|
|
178
|
-
branch_input,
|
|
179
|
-
step_name: name,
|
|
180
|
-
&block
|
|
181
|
-
)
|
|
182
|
-
|
|
183
|
-
mutex.synchronize do
|
|
184
|
-
results[name] = result
|
|
185
|
-
|
|
186
|
-
# Check for failure and trigger fail-fast if needed
|
|
187
|
-
if result.respond_to?(:error?) && result.error? && !config[:optional]
|
|
188
|
-
pool.abort! if self.class.fail_fast?
|
|
189
|
-
end
|
|
190
|
-
end
|
|
191
|
-
rescue StandardError => e
|
|
192
|
-
mutex.synchronize do
|
|
193
|
-
errors[name] = e
|
|
194
|
-
results[name] = nil
|
|
195
|
-
pool.abort! if self.class.fail_fast? && !config[:optional]
|
|
196
|
-
end
|
|
197
|
-
end
|
|
198
|
-
end
|
|
199
|
-
end
|
|
200
|
-
|
|
201
|
-
# Wait for all branches to complete
|
|
202
|
-
pool.wait_for_completion
|
|
203
|
-
pool.shutdown
|
|
204
|
-
|
|
205
|
-
# Determine overall status
|
|
206
|
-
status = determine_parallel_status(results, errors)
|
|
207
|
-
|
|
208
|
-
# Aggregate results
|
|
209
|
-
final_content = begin
|
|
210
|
-
aggregate(results)
|
|
211
|
-
rescue StandardError => e
|
|
212
|
-
errors[:aggregate] = e
|
|
213
|
-
results.transform_values { |r| r&.content }
|
|
214
|
-
end
|
|
215
|
-
|
|
216
|
-
build_parallel_result(
|
|
217
|
-
content: final_content,
|
|
218
|
-
branches: results,
|
|
219
|
-
errors: errors,
|
|
220
|
-
status: status
|
|
221
|
-
)
|
|
222
|
-
end
|
|
223
|
-
|
|
224
|
-
# Builds input for a specific branch
|
|
225
|
-
#
|
|
226
|
-
# @param name [Symbol] Branch name
|
|
227
|
-
# @param config [Hash] Branch configuration
|
|
228
|
-
# @return [Hash] Input for the branch
|
|
229
|
-
def build_branch_input(name, config)
|
|
230
|
-
if config[:input]
|
|
231
|
-
config[:input].call(options)
|
|
232
|
-
elsif respond_to?(:"before_#{name}", true)
|
|
233
|
-
send(:"before_#{name}", options)
|
|
234
|
-
else
|
|
235
|
-
options.dup
|
|
236
|
-
end
|
|
237
|
-
end
|
|
238
|
-
|
|
239
|
-
# Determines the overall parallel status
|
|
240
|
-
#
|
|
241
|
-
# @param results [Hash] Branch results
|
|
242
|
-
# @param errors [Hash] Branch errors
|
|
243
|
-
# @return [String] Status: "success", "partial", or "error"
|
|
244
|
-
def determine_parallel_status(results, errors)
|
|
245
|
-
required_branches = self.class.branches.reject { |_, c| c[:optional] }.keys
|
|
246
|
-
failed_required = required_branches.select do |name|
|
|
247
|
-
errors[name] || (results[name].respond_to?(:error?) && results[name].error?)
|
|
248
|
-
end
|
|
249
|
-
|
|
250
|
-
if failed_required.any?
|
|
251
|
-
"error"
|
|
252
|
-
elsif errors.any?
|
|
253
|
-
"partial"
|
|
254
|
-
else
|
|
255
|
-
"success"
|
|
256
|
-
end
|
|
257
|
-
end
|
|
258
|
-
|
|
259
|
-
# Builds the final parallel result
|
|
260
|
-
#
|
|
261
|
-
# @param content [Object] Aggregated content
|
|
262
|
-
# @param branches [Hash] Branch results
|
|
263
|
-
# @param errors [Hash] Branch errors
|
|
264
|
-
# @param status [String] Final status
|
|
265
|
-
# @return [WorkflowResult] The parallel result
|
|
266
|
-
def build_parallel_result(content:, branches:, errors:, status:)
|
|
267
|
-
Workflow::Result.new(
|
|
268
|
-
content: content,
|
|
269
|
-
workflow_type: self.class.name,
|
|
270
|
-
workflow_id: workflow_id,
|
|
271
|
-
branches: branches,
|
|
272
|
-
errors: errors,
|
|
273
|
-
status: status,
|
|
274
|
-
started_at: @workflow_started_at,
|
|
275
|
-
completed_at: Time.current,
|
|
276
|
-
duration_ms: (((Time.current - @workflow_started_at) * 1000).round if @workflow_started_at)
|
|
277
|
-
)
|
|
278
|
-
end
|
|
279
|
-
|
|
280
|
-
# Creates the appropriate executor based on context
|
|
281
|
-
#
|
|
282
|
-
# Uses AsyncExecutor when in async context for fiber-based concurrency,
|
|
283
|
-
# otherwise falls back to ThreadPool for traditional thread-based execution.
|
|
284
|
-
#
|
|
285
|
-
# @param size [Integer] Number of concurrent workers/fibers
|
|
286
|
-
# @return [ThreadPool, AsyncExecutor] The executor instance
|
|
287
|
-
def create_executor(size)
|
|
288
|
-
config = RubyLLM::Agents.configuration
|
|
289
|
-
|
|
290
|
-
if config.async_context?
|
|
291
|
-
AsyncExecutor.new(max_concurrent: size)
|
|
292
|
-
else
|
|
293
|
-
ThreadPool.new(size: size)
|
|
294
|
-
end
|
|
295
|
-
end
|
|
296
|
-
end
|
|
297
|
-
end
|
|
298
|
-
end
|
|
299
|
-
end
|
|
@@ -1,306 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module RubyLLM
|
|
4
|
-
module Agents
|
|
5
|
-
class Workflow
|
|
6
|
-
# Sequential workflow execution pattern
|
|
7
|
-
#
|
|
8
|
-
# Executes agents in order, passing each step's output as input to the next.
|
|
9
|
-
# Supports conditional step skipping, error handling, and input transformation.
|
|
10
|
-
#
|
|
11
|
-
# @example Basic pipeline
|
|
12
|
-
# class ContentPipeline < RubyLLM::Agents::Workflow::Pipeline
|
|
13
|
-
# version "1.0"
|
|
14
|
-
#
|
|
15
|
-
# step :extract, agent: ExtractorAgent
|
|
16
|
-
# step :validate, agent: ValidatorAgent
|
|
17
|
-
# step :format, agent: FormatterAgent
|
|
18
|
-
# end
|
|
19
|
-
#
|
|
20
|
-
# result = ContentPipeline.call(text: "raw input")
|
|
21
|
-
# result.steps[:extract].content # First step output
|
|
22
|
-
# result.content # Final output
|
|
23
|
-
#
|
|
24
|
-
# @example With conditional skipping
|
|
25
|
-
# class ConditionalPipeline < RubyLLM::Agents::Workflow::Pipeline
|
|
26
|
-
# step :check, agent: CheckerAgent
|
|
27
|
-
# step :process, agent: ProcessorAgent, skip_on: ->(ctx) { ctx[:check].content[:skip] }
|
|
28
|
-
# step :finalize, agent: FinalizerAgent
|
|
29
|
-
# end
|
|
30
|
-
#
|
|
31
|
-
# @example With input transformation
|
|
32
|
-
# class TransformPipeline < RubyLLM::Agents::Workflow::Pipeline
|
|
33
|
-
# step :analyze, agent: AnalyzerAgent
|
|
34
|
-
# step :enrich, agent: EnricherAgent
|
|
35
|
-
#
|
|
36
|
-
# def before_enrich(context)
|
|
37
|
-
# { data: context[:analyze].content, extra_field: "value" }
|
|
38
|
-
# end
|
|
39
|
-
# end
|
|
40
|
-
#
|
|
41
|
-
# @api public
|
|
42
|
-
class Pipeline < Workflow
|
|
43
|
-
# Simple error result for failed steps
|
|
44
|
-
# @api private
|
|
45
|
-
class ErrorResult
|
|
46
|
-
attr_reader :error_class, :error_message, :step_name
|
|
47
|
-
|
|
48
|
-
def initialize(step_name:, error_class:, error_message:)
|
|
49
|
-
@step_name = step_name
|
|
50
|
-
@error_class = error_class
|
|
51
|
-
@error_message = error_message
|
|
52
|
-
end
|
|
53
|
-
|
|
54
|
-
def content = nil
|
|
55
|
-
def success? = false
|
|
56
|
-
def error? = true
|
|
57
|
-
def input_tokens = 0
|
|
58
|
-
def output_tokens = 0
|
|
59
|
-
def total_tokens = 0
|
|
60
|
-
def cached_tokens = 0
|
|
61
|
-
def input_cost = 0.0
|
|
62
|
-
def output_cost = 0.0
|
|
63
|
-
def total_cost = 0.0
|
|
64
|
-
|
|
65
|
-
def to_h
|
|
66
|
-
{
|
|
67
|
-
error: true,
|
|
68
|
-
step_name: step_name,
|
|
69
|
-
error_class: error_class,
|
|
70
|
-
error_message: error_message
|
|
71
|
-
}
|
|
72
|
-
end
|
|
73
|
-
end
|
|
74
|
-
|
|
75
|
-
class << self
|
|
76
|
-
# Returns the defined steps
|
|
77
|
-
#
|
|
78
|
-
# @return [Hash<Symbol, Hash>] Step configurations
|
|
79
|
-
def steps
|
|
80
|
-
@steps ||= {}
|
|
81
|
-
end
|
|
82
|
-
|
|
83
|
-
# Inherits steps from parent class
|
|
84
|
-
def inherited(subclass)
|
|
85
|
-
super
|
|
86
|
-
subclass.instance_variable_set(:@steps, steps.dup)
|
|
87
|
-
end
|
|
88
|
-
|
|
89
|
-
# Defines a pipeline step
|
|
90
|
-
#
|
|
91
|
-
# @param name [Symbol] Step identifier
|
|
92
|
-
# @param agent [Class] The agent class to execute
|
|
93
|
-
# @param skip_on [Proc, nil] Lambda to determine if step should be skipped
|
|
94
|
-
# @param continue_on_error [Boolean] Whether to continue if step fails
|
|
95
|
-
# @param optional [Boolean] Mark step as optional (alias for continue_on_error)
|
|
96
|
-
# @return [void]
|
|
97
|
-
#
|
|
98
|
-
# @example Basic step
|
|
99
|
-
# step :process, agent: ProcessorAgent
|
|
100
|
-
#
|
|
101
|
-
# @example With skip condition
|
|
102
|
-
# step :validate, agent: ValidatorAgent, skip_on: ->(ctx) { ctx[:input][:skip_validation] }
|
|
103
|
-
#
|
|
104
|
-
# @example Optional step (won't fail pipeline)
|
|
105
|
-
# step :enrich, agent: EnricherAgent, optional: true
|
|
106
|
-
def step(name, agent:, skip_on: nil, continue_on_error: false, optional: false)
|
|
107
|
-
steps[name] = {
|
|
108
|
-
agent: agent,
|
|
109
|
-
skip_on: skip_on,
|
|
110
|
-
continue_on_error: continue_on_error || optional,
|
|
111
|
-
index: steps.size
|
|
112
|
-
}
|
|
113
|
-
end
|
|
114
|
-
end
|
|
115
|
-
|
|
116
|
-
# Executes the pipeline
|
|
117
|
-
#
|
|
118
|
-
# Runs each step sequentially, passing output to the next step.
|
|
119
|
-
# Tracks all step results and builds aggregate metrics.
|
|
120
|
-
#
|
|
121
|
-
# @yield [chunk] Yields chunks when streaming (passed to individual agents)
|
|
122
|
-
# @return [WorkflowResult] The pipeline result
|
|
123
|
-
def call(&block)
|
|
124
|
-
instrument_workflow do
|
|
125
|
-
execute_pipeline(&block)
|
|
126
|
-
end
|
|
127
|
-
end
|
|
128
|
-
|
|
129
|
-
private
|
|
130
|
-
|
|
131
|
-
# Executes the pipeline steps
|
|
132
|
-
#
|
|
133
|
-
# @return [WorkflowResult] The pipeline result
|
|
134
|
-
def execute_pipeline(&block)
|
|
135
|
-
context = { input: options }
|
|
136
|
-
step_results = {}
|
|
137
|
-
errors = {}
|
|
138
|
-
last_successful_result = nil
|
|
139
|
-
status = "success"
|
|
140
|
-
|
|
141
|
-
self.class.steps.each do |name, config|
|
|
142
|
-
# Check skip condition
|
|
143
|
-
if should_skip_step?(config, context)
|
|
144
|
-
step_results[name] = SkippedResult.new(name, reason: "skip_on condition met")
|
|
145
|
-
next
|
|
146
|
-
end
|
|
147
|
-
|
|
148
|
-
begin
|
|
149
|
-
# Build input for this step
|
|
150
|
-
step_input = before_step(name, context)
|
|
151
|
-
|
|
152
|
-
# Execute the step
|
|
153
|
-
result = execute_agent(
|
|
154
|
-
config[:agent],
|
|
155
|
-
step_input,
|
|
156
|
-
step_name: name,
|
|
157
|
-
&block
|
|
158
|
-
)
|
|
159
|
-
|
|
160
|
-
step_results[name] = result
|
|
161
|
-
context[name] = result
|
|
162
|
-
last_successful_result = result
|
|
163
|
-
|
|
164
|
-
# Check if step failed
|
|
165
|
-
if result.respond_to?(:error?) && result.error?
|
|
166
|
-
errors[name] = StandardError.new(result.error_message || "Step failed")
|
|
167
|
-
|
|
168
|
-
unless config[:continue_on_error]
|
|
169
|
-
status = "error"
|
|
170
|
-
break
|
|
171
|
-
end
|
|
172
|
-
|
|
173
|
-
status = "partial" if status == "success"
|
|
174
|
-
end
|
|
175
|
-
rescue StandardError => e
|
|
176
|
-
# Handle step execution errors
|
|
177
|
-
errors[name] = e
|
|
178
|
-
step_results[name] = build_error_result(name, e)
|
|
179
|
-
context[name] = step_results[name]
|
|
180
|
-
|
|
181
|
-
# Check continue_on_error first - if true, continue without calling handler
|
|
182
|
-
if config[:continue_on_error]
|
|
183
|
-
status = "partial" if status == "success"
|
|
184
|
-
next
|
|
185
|
-
end
|
|
186
|
-
|
|
187
|
-
# Call error handler hook for non-optional steps
|
|
188
|
-
action = on_step_failure(name, e, context)
|
|
189
|
-
|
|
190
|
-
case action
|
|
191
|
-
when :skip
|
|
192
|
-
status = "partial" if status == "success"
|
|
193
|
-
next
|
|
194
|
-
when :abort
|
|
195
|
-
status = "error"
|
|
196
|
-
break
|
|
197
|
-
when Result, Workflow::Result
|
|
198
|
-
# Use the returned result as the step result
|
|
199
|
-
step_results[name] = action
|
|
200
|
-
context[name] = action
|
|
201
|
-
last_successful_result = action
|
|
202
|
-
status = "partial" if status == "success"
|
|
203
|
-
else
|
|
204
|
-
status = "error"
|
|
205
|
-
break
|
|
206
|
-
end
|
|
207
|
-
end
|
|
208
|
-
end
|
|
209
|
-
|
|
210
|
-
# Build final content from last successful step
|
|
211
|
-
final_content = extract_final_content(last_successful_result, context)
|
|
212
|
-
|
|
213
|
-
build_pipeline_result(
|
|
214
|
-
content: final_content,
|
|
215
|
-
steps: step_results,
|
|
216
|
-
errors: errors,
|
|
217
|
-
status: status
|
|
218
|
-
)
|
|
219
|
-
end
|
|
220
|
-
|
|
221
|
-
# Checks if a step should be skipped
|
|
222
|
-
#
|
|
223
|
-
# @param config [Hash] Step configuration
|
|
224
|
-
# @param context [Hash] Current workflow context
|
|
225
|
-
# @return [Boolean] true if step should be skipped
|
|
226
|
-
def should_skip_step?(config, context)
|
|
227
|
-
return false unless config[:skip_on]
|
|
228
|
-
|
|
229
|
-
config[:skip_on].call(context)
|
|
230
|
-
rescue StandardError => e
|
|
231
|
-
Rails.logger.warn("[RubyLLM::Agents::Pipeline] skip_on evaluation failed: #{e.message}")
|
|
232
|
-
false
|
|
233
|
-
end
|
|
234
|
-
|
|
235
|
-
# Hook for handling step failures
|
|
236
|
-
#
|
|
237
|
-
# Override in subclass to customize error handling.
|
|
238
|
-
#
|
|
239
|
-
# @param step_name [Symbol] The failed step
|
|
240
|
-
# @param error [Exception] The error
|
|
241
|
-
# @param context [Hash] Current workflow context
|
|
242
|
-
# @return [Symbol, Result] :skip to continue, :abort to stop, or a fallback Result
|
|
243
|
-
def on_step_failure(step_name, error, context)
|
|
244
|
-
# Default: check if method exists for specific step
|
|
245
|
-
method_name = :"on_#{step_name}_failure"
|
|
246
|
-
if respond_to?(method_name, true)
|
|
247
|
-
send(method_name, error, context)
|
|
248
|
-
else
|
|
249
|
-
:abort
|
|
250
|
-
end
|
|
251
|
-
end
|
|
252
|
-
|
|
253
|
-
# Extracts the final content from the pipeline
|
|
254
|
-
#
|
|
255
|
-
# @param last_result [Result, nil] The last successful result
|
|
256
|
-
# @param context [Hash] The workflow context
|
|
257
|
-
# @return [Object] The final content
|
|
258
|
-
def extract_final_content(last_result, context)
|
|
259
|
-
if last_result.respond_to?(:content)
|
|
260
|
-
last_result.content
|
|
261
|
-
elsif context.keys.size > 1
|
|
262
|
-
# Return the last non-input context entry
|
|
263
|
-
last_key = context.keys.reject { |k| k == :input }.last
|
|
264
|
-
context[last_key]&.content
|
|
265
|
-
else
|
|
266
|
-
nil
|
|
267
|
-
end
|
|
268
|
-
end
|
|
269
|
-
|
|
270
|
-
# Builds an error result for a failed step
|
|
271
|
-
#
|
|
272
|
-
# @param step_name [Symbol] The step name
|
|
273
|
-
# @param error [Exception] The error
|
|
274
|
-
# @return [ErrorResult] Error result object
|
|
275
|
-
def build_error_result(step_name, error)
|
|
276
|
-
ErrorResult.new(
|
|
277
|
-
step_name: step_name,
|
|
278
|
-
error_class: error.class.name,
|
|
279
|
-
error_message: error.message
|
|
280
|
-
)
|
|
281
|
-
end
|
|
282
|
-
|
|
283
|
-
# Builds the final pipeline result
|
|
284
|
-
#
|
|
285
|
-
# @param content [Object] Final content
|
|
286
|
-
# @param steps [Hash] Step results
|
|
287
|
-
# @param errors [Hash] Step errors
|
|
288
|
-
# @param status [String] Final status
|
|
289
|
-
# @return [WorkflowResult] The pipeline result
|
|
290
|
-
def build_pipeline_result(content:, steps:, errors:, status:)
|
|
291
|
-
Workflow::Result.new(
|
|
292
|
-
content: content,
|
|
293
|
-
workflow_type: self.class.name,
|
|
294
|
-
workflow_id: workflow_id,
|
|
295
|
-
steps: steps,
|
|
296
|
-
errors: errors,
|
|
297
|
-
status: status,
|
|
298
|
-
started_at: @workflow_started_at,
|
|
299
|
-
completed_at: Time.current,
|
|
300
|
-
duration_ms: (((Time.current - @workflow_started_at) * 1000).round if @workflow_started_at)
|
|
301
|
-
)
|
|
302
|
-
end
|
|
303
|
-
end
|
|
304
|
-
end
|
|
305
|
-
end
|
|
306
|
-
end
|