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
|
@@ -0,0 +1,289 @@
|
|
|
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
|
|
@@ -0,0 +1,107 @@
|
|
|
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
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
module Agents
|
|
5
|
+
class Workflow
|
|
6
|
+
module DSL
|
|
7
|
+
# Builder for defining routing options in a step
|
|
8
|
+
#
|
|
9
|
+
# Used with the `on:` option to route to different agents based on
|
|
10
|
+
# a runtime value. Supports a fluent interface for defining routes.
|
|
11
|
+
#
|
|
12
|
+
# @example Basic routing
|
|
13
|
+
# step :process, on: -> { enrich.tier } do |route|
|
|
14
|
+
# route.premium PremiumAgent
|
|
15
|
+
# route.standard StandardAgent
|
|
16
|
+
# route.default DefaultAgent
|
|
17
|
+
# end
|
|
18
|
+
#
|
|
19
|
+
# @example With per-route options
|
|
20
|
+
# step :process, on: -> { enrich.tier } do |route|
|
|
21
|
+
# route.premium PremiumAgent, input: -> { { vip: true } }, timeout: 5.minutes
|
|
22
|
+
# route.standard StandardAgent
|
|
23
|
+
# route.default DefaultAgent
|
|
24
|
+
# end
|
|
25
|
+
#
|
|
26
|
+
# @api private
|
|
27
|
+
class RouteBuilder
|
|
28
|
+
# Error raised when no route matches and no default is defined
|
|
29
|
+
class NoRouteError < StandardError
|
|
30
|
+
attr_reader :value, :available_routes
|
|
31
|
+
|
|
32
|
+
def initialize(message, value: nil, available_routes: [])
|
|
33
|
+
super(message)
|
|
34
|
+
@value = value
|
|
35
|
+
@available_routes = available_routes
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def initialize
|
|
40
|
+
@routes = {}
|
|
41
|
+
@default = nil
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Returns all defined routes
|
|
45
|
+
#
|
|
46
|
+
# @return [Hash<Symbol, Hash>]
|
|
47
|
+
attr_reader :routes
|
|
48
|
+
|
|
49
|
+
# Returns or sets the default route
|
|
50
|
+
#
|
|
51
|
+
# When called with no arguments, returns the current default.
|
|
52
|
+
# When called with an agent, sets the default route.
|
|
53
|
+
#
|
|
54
|
+
# @param agent [Class, nil] Agent class for the default route
|
|
55
|
+
# @param options [Hash] Route options
|
|
56
|
+
# @return [Hash, nil]
|
|
57
|
+
def default(agent = nil, **options)
|
|
58
|
+
if agent.nil? && options.empty?
|
|
59
|
+
@default
|
|
60
|
+
else
|
|
61
|
+
@default = { agent: agent, options: options }
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Handles dynamic route definitions
|
|
66
|
+
#
|
|
67
|
+
# Any method call becomes a route definition.
|
|
68
|
+
#
|
|
69
|
+
# @param name [Symbol] Route name
|
|
70
|
+
# @param agent [Class] Agent class for this route
|
|
71
|
+
# @param options [Hash] Route options
|
|
72
|
+
# @return [void]
|
|
73
|
+
def method_missing(name, agent = nil, **options)
|
|
74
|
+
if name == :default
|
|
75
|
+
@default = { agent: agent, options: options }
|
|
76
|
+
else
|
|
77
|
+
@routes[name.to_sym] = { agent: agent, options: options }
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def respond_to_missing?(name, include_private = false)
|
|
82
|
+
true
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Resolves the route for a given value
|
|
86
|
+
#
|
|
87
|
+
# @param value [Object] The routing key value
|
|
88
|
+
# @return [Hash] Route configuration with :agent and :options
|
|
89
|
+
# @raise [NoRouteError] If no route matches and no default is defined
|
|
90
|
+
def resolve(value)
|
|
91
|
+
key = normalize_key(value)
|
|
92
|
+
|
|
93
|
+
route = @routes[key] || @default
|
|
94
|
+
|
|
95
|
+
unless route
|
|
96
|
+
raise NoRouteError.new(
|
|
97
|
+
"No route defined for value: #{value.inspect} (normalized: #{key}). " \
|
|
98
|
+
"Available routes: #{@routes.keys.join(', ')}",
|
|
99
|
+
value: value,
|
|
100
|
+
available_routes: @routes.keys
|
|
101
|
+
)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
route
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# Returns all route names
|
|
108
|
+
#
|
|
109
|
+
# @return [Array<Symbol>]
|
|
110
|
+
def route_names
|
|
111
|
+
@routes.keys
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Checks if a route exists
|
|
115
|
+
#
|
|
116
|
+
# @param name [Symbol] Route name
|
|
117
|
+
# @return [Boolean]
|
|
118
|
+
def route_exists?(name)
|
|
119
|
+
@routes.key?(name.to_sym) || @default.present?
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# Converts to hash for serialization
|
|
123
|
+
#
|
|
124
|
+
# @return [Hash]
|
|
125
|
+
def to_h
|
|
126
|
+
{
|
|
127
|
+
routes: @routes.transform_values do |r|
|
|
128
|
+
{ agent: r[:agent]&.name, options: r[:options] }
|
|
129
|
+
end,
|
|
130
|
+
default: @default ? { agent: @default[:agent]&.name, options: @default[:options] } : nil
|
|
131
|
+
}
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
private
|
|
135
|
+
|
|
136
|
+
def normalize_key(value)
|
|
137
|
+
case value
|
|
138
|
+
when Symbol then value
|
|
139
|
+
when String then value.to_sym
|
|
140
|
+
when TrueClass then :true
|
|
141
|
+
when FalseClass then :false
|
|
142
|
+
when NilClass then :nil
|
|
143
|
+
else value.to_s.to_sym
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
end
|