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.
- checksums.yaml +4 -4
- data/README.md +112 -336
- data/app/controllers/concerns/ruby_llm/agents/sortable.rb +0 -1
- data/app/controllers/ruby_llm/agents/agents_controller.rb +5 -56
- data/app/controllers/ruby_llm/agents/dashboard_controller.rb +22 -106
- data/app/controllers/ruby_llm/agents/executions_controller.rb +4 -114
- data/app/controllers/ruby_llm/agents/tenants_controller.rb +30 -2
- data/app/helpers/ruby_llm/agents/application_helper.rb +19 -53
- data/app/models/ruby_llm/agents/execution/analytics.rb +13 -54
- data/app/models/ruby_llm/agents/execution/scopes.rb +61 -14
- data/app/models/ruby_llm/agents/execution.rb +52 -12
- data/app/models/ruby_llm/agents/execution_detail.rb +18 -0
- data/app/models/ruby_llm/agents/tenant/budgetable.rb +132 -24
- data/app/models/ruby_llm/agents/tenant/incrementable.rb +117 -0
- data/app/models/ruby_llm/agents/tenant/resettable.rb +128 -0
- data/app/models/ruby_llm/agents/tenant/trackable.rb +46 -12
- data/app/models/ruby_llm/agents/tenant.rb +2 -3
- data/app/models/ruby_llm/agents/tenant_budget.rb +6 -3
- data/app/services/ruby_llm/agents/agent_registry.rb +6 -112
- data/app/views/layouts/ruby_llm/agents/application.html.erb +89 -252
- data/app/views/ruby_llm/agents/agents/_config_agent.html.erb +71 -218
- data/app/views/ruby_llm/agents/agents/_config_embedder.html.erb +20 -63
- data/app/views/ruby_llm/agents/agents/_config_image_generator.html.erb +44 -131
- data/app/views/ruby_llm/agents/agents/_config_moderator.html.erb +16 -57
- data/app/views/ruby_llm/agents/agents/_config_speaker.html.erb +39 -104
- data/app/views/ruby_llm/agents/agents/_config_transcriber.html.erb +29 -82
- data/app/views/ruby_llm/agents/agents/_empty_state.html.erb +4 -14
- data/app/views/ruby_llm/agents/agents/index.html.erb +105 -274
- data/app/views/ruby_llm/agents/agents/show.html.erb +248 -378
- data/app/views/ruby_llm/agents/dashboard/_action_center.html.erb +29 -52
- data/app/views/ruby_llm/agents/dashboard/_tenant_budget.html.erb +73 -99
- data/app/views/ruby_llm/agents/dashboard/index.html.erb +228 -433
- data/app/views/ruby_llm/agents/executions/_execution.html.erb +1 -1
- data/app/views/ruby_llm/agents/executions/_filters.html.erb +4 -25
- data/app/views/ruby_llm/agents/executions/_list.html.erb +111 -152
- data/app/views/ruby_llm/agents/executions/index.html.erb +5 -7
- data/app/views/ruby_llm/agents/executions/show.html.erb +526 -1037
- data/app/views/ruby_llm/agents/shared/_agent_type_badge.html.erb +5 -21
- data/app/views/ruby_llm/agents/shared/_executions_table.html.erb +70 -191
- data/app/views/ruby_llm/agents/shared/_filter_dropdown.html.erb +16 -44
- data/app/views/ruby_llm/agents/shared/_select_dropdown.html.erb +12 -41
- data/app/views/ruby_llm/agents/shared/_status_badge.html.erb +11 -65
- data/app/views/ruby_llm/agents/shared/_tenant_filter.html.erb +6 -5
- data/app/views/ruby_llm/agents/system_config/show.html.erb +240 -351
- data/app/views/ruby_llm/agents/tenants/_form.html.erb +67 -77
- data/app/views/ruby_llm/agents/tenants/edit.html.erb +7 -9
- data/app/views/ruby_llm/agents/tenants/index.html.erb +100 -122
- data/app/views/ruby_llm/agents/tenants/show.html.erb +146 -336
- data/config/routes.rb +0 -13
- data/lib/generators/ruby_llm_agents/install_generator.rb +13 -17
- data/lib/generators/ruby_llm_agents/migrate_structure_generator.rb +2 -12
- data/lib/generators/ruby_llm_agents/restructure_generator.rb +0 -2
- data/lib/generators/ruby_llm_agents/templates/add_usage_counters_to_tenants_migration.rb.tt +37 -0
- data/lib/generators/ruby_llm_agents/templates/agent.rb.tt +1 -2
- data/lib/generators/ruby_llm_agents/templates/application_agent.rb.tt +1 -1
- data/lib/generators/ruby_llm_agents/templates/application_image_pipeline.rb.tt +0 -1
- data/lib/generators/ruby_llm_agents/templates/create_execution_details_migration.rb.tt +27 -0
- data/lib/generators/ruby_llm_agents/templates/create_tenants_migration.rb.tt +25 -0
- data/lib/generators/ruby_llm_agents/templates/image_pipeline.rb.tt +0 -1
- data/lib/generators/ruby_llm_agents/templates/initializer.rb.tt +33 -12
- data/lib/generators/ruby_llm_agents/templates/migration.rb.tt +40 -71
- data/lib/generators/ruby_llm_agents/templates/remove_agent_version_migration.rb.tt +13 -0
- data/lib/generators/ruby_llm_agents/templates/remove_workflow_columns_migration.rb.tt +19 -0
- data/lib/generators/ruby_llm_agents/templates/skills/AGENTS.md.tt +2 -4
- data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_PIPELINES.md.tt +0 -1
- data/lib/generators/ruby_llm_agents/templates/split_execution_details_migration.rb.tt +232 -0
- data/lib/generators/ruby_llm_agents/upgrade_generator.rb +77 -259
- data/lib/ruby_llm/agents/audio/speaker.rb +0 -1
- data/lib/ruby_llm/agents/audio/transcriber.rb +0 -1
- data/lib/ruby_llm/agents/base_agent.rb +54 -23
- data/lib/ruby_llm/agents/core/base/callbacks.rb +142 -0
- data/lib/ruby_llm/agents/core/base.rb +23 -55
- data/lib/ruby_llm/agents/core/configuration.rb +97 -117
- data/lib/ruby_llm/agents/core/errors.rb +0 -58
- data/lib/ruby_llm/agents/core/instrumentation.rb +157 -110
- data/lib/ruby_llm/agents/core/llm_tenant.rb +8 -7
- data/lib/ruby_llm/agents/core/version.rb +1 -1
- data/lib/ruby_llm/agents/dsl/base.rb +157 -17
- data/lib/ruby_llm/agents/dsl/caching.rb +33 -2
- data/lib/ruby_llm/agents/dsl/reliability.rb +148 -0
- data/lib/ruby_llm/agents/dsl.rb +1 -2
- data/lib/ruby_llm/agents/image/analyzer/execution.rb +1 -2
- data/lib/ruby_llm/agents/image/background_remover/execution.rb +1 -2
- data/lib/ruby_llm/agents/image/concerns/image_operation_dsl.rb +1 -13
- data/lib/ruby_llm/agents/image/concerns/image_operation_execution.rb +2 -2
- data/lib/ruby_llm/agents/image/editor/dsl.rb +0 -14
- data/lib/ruby_llm/agents/image/editor/execution.rb +1 -10
- data/lib/ruby_llm/agents/image/editor.rb +0 -1
- data/lib/ruby_llm/agents/image/generator.rb +0 -21
- data/lib/ruby_llm/agents/image/pipeline/dsl.rb +0 -13
- data/lib/ruby_llm/agents/image/pipeline/execution.rb +0 -1
- data/lib/ruby_llm/agents/image/transformer/dsl.rb +0 -13
- data/lib/ruby_llm/agents/image/transformer/execution.rb +1 -10
- data/lib/ruby_llm/agents/image/transformer.rb +0 -1
- data/lib/ruby_llm/agents/image/upscaler/execution.rb +1 -2
- data/lib/ruby_llm/agents/image/variator/execution.rb +1 -2
- data/lib/ruby_llm/agents/infrastructure/alert_manager.rb +78 -173
- data/lib/ruby_llm/agents/infrastructure/budget/budget_query.rb +66 -2
- data/lib/ruby_llm/agents/infrastructure/budget/spend_recorder.rb +0 -12
- data/lib/ruby_llm/agents/infrastructure/circuit_breaker.rb +10 -13
- data/lib/ruby_llm/agents/infrastructure/execution_logger_job.rb +8 -0
- data/lib/ruby_llm/agents/pipeline/context.rb +0 -1
- data/lib/ruby_llm/agents/pipeline/middleware/budget.rb +28 -4
- data/lib/ruby_llm/agents/pipeline/middleware/cache.rb +3 -10
- data/lib/ruby_llm/agents/pipeline/middleware/instrumentation.rb +88 -55
- data/lib/ruby_llm/agents/pipeline/middleware/tenant.rb +5 -41
- data/lib/ruby_llm/agents/rails/engine.rb +6 -6
- data/lib/ruby_llm/agents/results/base.rb +1 -49
- data/lib/ruby_llm/agents/text/embedder.rb +0 -1
- data/lib/ruby_llm/agents.rb +1 -9
- data/lib/tasks/ruby_llm_agents.rake +34 -0
- metadata +14 -83
- data/app/controllers/ruby_llm/agents/api_configurations_controller.rb +0 -214
- data/app/controllers/ruby_llm/agents/workflows_controller.rb +0 -544
- data/app/mailers/ruby_llm/agents/alert_mailer.rb +0 -84
- data/app/mailers/ruby_llm/agents/application_mailer.rb +0 -28
- data/app/models/ruby_llm/agents/api_configuration.rb +0 -386
- data/app/models/ruby_llm/agents/execution/workflow.rb +0 -170
- data/app/models/ruby_llm/agents/tenant/configurable.rb +0 -135
- data/app/views/ruby_llm/agents/agents/_agent.html.erb +0 -98
- data/app/views/ruby_llm/agents/agents/_version_comparison.html.erb +0 -186
- data/app/views/ruby_llm/agents/agents/_workflow.html.erb +0 -126
- data/app/views/ruby_llm/agents/alert_mailer/alert_notification.html.erb +0 -107
- data/app/views/ruby_llm/agents/alert_mailer/alert_notification.text.erb +0 -18
- data/app/views/ruby_llm/agents/api_configurations/_api_key_field.html.erb +0 -34
- data/app/views/ruby_llm/agents/api_configurations/_form.html.erb +0 -288
- data/app/views/ruby_llm/agents/api_configurations/edit.html.erb +0 -95
- data/app/views/ruby_llm/agents/api_configurations/edit_tenant.html.erb +0 -97
- data/app/views/ruby_llm/agents/api_configurations/show.html.erb +0 -214
- data/app/views/ruby_llm/agents/api_configurations/tenant.html.erb +0 -179
- data/app/views/ruby_llm/agents/dashboard/_agent_comparison.html.erb +0 -73
- data/app/views/ruby_llm/agents/dashboard/_alerts_feed.html.erb +0 -62
- data/app/views/ruby_llm/agents/dashboard/_breaker_strip.html.erb +0 -47
- data/app/views/ruby_llm/agents/dashboard/_budgets_bar.html.erb +0 -75
- data/app/views/ruby_llm/agents/dashboard/_model_comparison.html.erb +0 -56
- data/app/views/ruby_llm/agents/dashboard/_model_cost_breakdown.html.erb +0 -115
- data/app/views/ruby_llm/agents/dashboard/_now_strip.html.erb +0 -59
- data/app/views/ruby_llm/agents/dashboard/_top_errors.html.erb +0 -60
- data/app/views/ruby_llm/agents/executions/_workflow_summary.html.erb +0 -86
- data/app/views/ruby_llm/agents/executions/dry_run.html.erb +0 -149
- data/app/views/ruby_llm/agents/shared/_breadcrumbs.html.erb +0 -48
- data/app/views/ruby_llm/agents/shared/_nav_link.html.erb +0 -27
- data/app/views/ruby_llm/agents/shared/_stat_card.html.erb +0 -14
- data/app/views/ruby_llm/agents/shared/_workflow_type_badge.html.erb +0 -35
- data/app/views/ruby_llm/agents/workflows/_empty_state.html.erb +0 -22
- data/app/views/ruby_llm/agents/workflows/_step_performance.html.erb +0 -228
- data/app/views/ruby_llm/agents/workflows/_structure_dsl.html.erb +0 -539
- data/app/views/ruby_llm/agents/workflows/_structure_parallel.html.erb +0 -76
- data/app/views/ruby_llm/agents/workflows/_structure_pipeline.html.erb +0 -74
- data/app/views/ruby_llm/agents/workflows/_structure_router.html.erb +0 -108
- data/app/views/ruby_llm/agents/workflows/_workflow_diagram.html.erb +0 -920
- data/app/views/ruby_llm/agents/workflows/index.html.erb +0 -179
- data/app/views/ruby_llm/agents/workflows/show.html.erb +0 -467
- data/lib/generators/ruby_llm_agents/api_configuration_generator.rb +0 -100
- data/lib/generators/ruby_llm_agents/templates/add_workflow_migration.rb.tt +0 -38
- data/lib/generators/ruby_llm_agents/templates/application_workflow.rb.tt +0 -48
- data/lib/generators/ruby_llm_agents/templates/create_api_configurations_migration.rb.tt +0 -90
- data/lib/generators/ruby_llm_agents/templates/skills/WORKFLOWS.md.tt +0 -551
- data/lib/ruby_llm/agents/core/base/moderation_dsl.rb +0 -181
- data/lib/ruby_llm/agents/core/base/moderation_execution.rb +0 -274
- data/lib/ruby_llm/agents/core/resolved_config.rb +0 -348
- data/lib/ruby_llm/agents/image/generator/content_policy.rb +0 -95
- data/lib/ruby_llm/agents/infrastructure/redactor.rb +0 -130
- data/lib/ruby_llm/agents/results/moderation_result.rb +0 -158
- data/lib/ruby_llm/agents/text/moderator.rb +0 -237
- data/lib/ruby_llm/agents/workflow/approval.rb +0 -205
- data/lib/ruby_llm/agents/workflow/approval_store.rb +0 -179
- data/lib/ruby_llm/agents/workflow/async.rb +0 -220
- data/lib/ruby_llm/agents/workflow/async_executor.rb +0 -156
- data/lib/ruby_llm/agents/workflow/dsl/executor.rb +0 -467
- data/lib/ruby_llm/agents/workflow/dsl/input_schema.rb +0 -244
- data/lib/ruby_llm/agents/workflow/dsl/iteration_executor.rb +0 -289
- data/lib/ruby_llm/agents/workflow/dsl/parallel_group.rb +0 -107
- data/lib/ruby_llm/agents/workflow/dsl/route_builder.rb +0 -150
- data/lib/ruby_llm/agents/workflow/dsl/schedule_helpers.rb +0 -187
- data/lib/ruby_llm/agents/workflow/dsl/step_config.rb +0 -352
- data/lib/ruby_llm/agents/workflow/dsl/step_executor.rb +0 -415
- data/lib/ruby_llm/agents/workflow/dsl/wait_config.rb +0 -257
- data/lib/ruby_llm/agents/workflow/dsl/wait_executor.rb +0 -317
- data/lib/ruby_llm/agents/workflow/dsl.rb +0 -576
- data/lib/ruby_llm/agents/workflow/instrumentation.rb +0 -249
- data/lib/ruby_llm/agents/workflow/notifiers/base.rb +0 -117
- data/lib/ruby_llm/agents/workflow/notifiers/email.rb +0 -117
- data/lib/ruby_llm/agents/workflow/notifiers/slack.rb +0 -180
- data/lib/ruby_llm/agents/workflow/notifiers/webhook.rb +0 -121
- data/lib/ruby_llm/agents/workflow/notifiers.rb +0 -70
- data/lib/ruby_llm/agents/workflow/orchestrator.rb +0 -416
- data/lib/ruby_llm/agents/workflow/result.rb +0 -592
- data/lib/ruby_llm/agents/workflow/thread_pool.rb +0 -185
- data/lib/ruby_llm/agents/workflow/throttle_manager.rb +0 -206
- data/lib/ruby_llm/agents/workflow/wait_result.rb +0 -213
|
@@ -1,156 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module RubyLLM
|
|
4
|
-
module Agents
|
|
5
|
-
class Workflow
|
|
6
|
-
# Fiber-based concurrent executor for parallel workflows
|
|
7
|
-
#
|
|
8
|
-
# Provides an alternative to ThreadPool that uses Ruby's Fiber scheduler
|
|
9
|
-
# for lightweight concurrency. Automatically used when the async gem is
|
|
10
|
-
# available and we're inside an async context.
|
|
11
|
-
#
|
|
12
|
-
# @example Basic usage
|
|
13
|
-
# executor = AsyncExecutor.new(max_concurrent: 4)
|
|
14
|
-
# executor.post { perform_task_1 }
|
|
15
|
-
# executor.post { perform_task_2 }
|
|
16
|
-
# executor.wait_for_completion
|
|
17
|
-
#
|
|
18
|
-
# @example With fail-fast
|
|
19
|
-
# executor = AsyncExecutor.new(max_concurrent: 4)
|
|
20
|
-
# executor.post { risky_task }
|
|
21
|
-
# executor.abort! if something_failed
|
|
22
|
-
# executor.wait_for_completion
|
|
23
|
-
#
|
|
24
|
-
# @api private
|
|
25
|
-
class AsyncExecutor
|
|
26
|
-
attr_reader :max_concurrent
|
|
27
|
-
|
|
28
|
-
# Creates a new async executor
|
|
29
|
-
#
|
|
30
|
-
# @param max_concurrent [Integer] Maximum concurrent fibers (default: 10)
|
|
31
|
-
def initialize(max_concurrent: 10)
|
|
32
|
-
@max_concurrent = max_concurrent
|
|
33
|
-
@tasks = []
|
|
34
|
-
@results = []
|
|
35
|
-
@mutex = Mutex.new
|
|
36
|
-
@aborted = false
|
|
37
|
-
@semaphore = nil
|
|
38
|
-
end
|
|
39
|
-
|
|
40
|
-
# Submits a task for execution
|
|
41
|
-
#
|
|
42
|
-
# @yield Block to execute
|
|
43
|
-
# @return [void]
|
|
44
|
-
def post(&block)
|
|
45
|
-
@mutex.synchronize do
|
|
46
|
-
@tasks << block
|
|
47
|
-
end
|
|
48
|
-
end
|
|
49
|
-
|
|
50
|
-
# Signals that remaining tasks should be skipped
|
|
51
|
-
#
|
|
52
|
-
# Currently running tasks will complete, but pending tasks will be skipped.
|
|
53
|
-
#
|
|
54
|
-
# @return [void]
|
|
55
|
-
def abort!
|
|
56
|
-
@mutex.synchronize do
|
|
57
|
-
@aborted = true
|
|
58
|
-
end
|
|
59
|
-
end
|
|
60
|
-
|
|
61
|
-
# Returns whether the executor has been aborted
|
|
62
|
-
#
|
|
63
|
-
# @return [Boolean] true if abort! was called
|
|
64
|
-
def aborted?
|
|
65
|
-
@mutex.synchronize { @aborted }
|
|
66
|
-
end
|
|
67
|
-
|
|
68
|
-
# Executes all submitted tasks and waits for completion
|
|
69
|
-
#
|
|
70
|
-
# @param timeout [Integer, nil] Maximum seconds to wait (nil = indefinite)
|
|
71
|
-
# @return [Boolean] true if all tasks completed, false if timeout
|
|
72
|
-
def wait_for_completion(timeout: nil)
|
|
73
|
-
return true if @tasks.empty?
|
|
74
|
-
|
|
75
|
-
ensure_async_available!
|
|
76
|
-
|
|
77
|
-
@semaphore = ::Async::Semaphore.new(@max_concurrent)
|
|
78
|
-
|
|
79
|
-
if timeout
|
|
80
|
-
execute_with_timeout(timeout)
|
|
81
|
-
else
|
|
82
|
-
execute_all
|
|
83
|
-
true
|
|
84
|
-
end
|
|
85
|
-
end
|
|
86
|
-
|
|
87
|
-
# Shuts down the executor
|
|
88
|
-
#
|
|
89
|
-
# For AsyncExecutor this is a no-op since fibers are garbage collected.
|
|
90
|
-
#
|
|
91
|
-
# @param timeout [Integer] Ignored for async executor
|
|
92
|
-
# @return [void]
|
|
93
|
-
def shutdown(timeout: 5)
|
|
94
|
-
# No-op for fiber-based executor
|
|
95
|
-
# Fibers are lightweight and garbage collected
|
|
96
|
-
end
|
|
97
|
-
|
|
98
|
-
# Waits for termination (compatibility with ThreadPool)
|
|
99
|
-
#
|
|
100
|
-
# @param timeout [Integer] Ignored for async executor
|
|
101
|
-
# @return [void]
|
|
102
|
-
def wait_for_termination(timeout: 5)
|
|
103
|
-
# No-op for fiber-based executor
|
|
104
|
-
end
|
|
105
|
-
|
|
106
|
-
private
|
|
107
|
-
|
|
108
|
-
# Executes all tasks with async
|
|
109
|
-
#
|
|
110
|
-
# @return [void]
|
|
111
|
-
def execute_all
|
|
112
|
-
Kernel.send(:Async) do
|
|
113
|
-
@tasks.map do |task|
|
|
114
|
-
Kernel.send(:Async) do
|
|
115
|
-
next if aborted?
|
|
116
|
-
|
|
117
|
-
@semaphore.acquire do
|
|
118
|
-
next if aborted?
|
|
119
|
-
task.call
|
|
120
|
-
end
|
|
121
|
-
end
|
|
122
|
-
end.map(&:wait)
|
|
123
|
-
end.wait
|
|
124
|
-
end
|
|
125
|
-
|
|
126
|
-
# Executes all tasks with a timeout
|
|
127
|
-
#
|
|
128
|
-
# @param timeout [Integer] Maximum seconds to wait
|
|
129
|
-
# @return [Boolean] true if completed, false if timeout
|
|
130
|
-
def execute_with_timeout(timeout)
|
|
131
|
-
completed = false
|
|
132
|
-
|
|
133
|
-
Kernel.send(:Async) do |task|
|
|
134
|
-
task.with_timeout(timeout) do
|
|
135
|
-
execute_all
|
|
136
|
-
completed = true
|
|
137
|
-
rescue ::Async::TimeoutError
|
|
138
|
-
completed = false
|
|
139
|
-
end
|
|
140
|
-
end.wait
|
|
141
|
-
|
|
142
|
-
completed
|
|
143
|
-
end
|
|
144
|
-
|
|
145
|
-
# Ensures async gem is available
|
|
146
|
-
#
|
|
147
|
-
# @raise [RuntimeError] If async gem is not loaded
|
|
148
|
-
def ensure_async_available!
|
|
149
|
-
return if defined?(::Async) && defined?(::Async::Semaphore)
|
|
150
|
-
|
|
151
|
-
raise "AsyncExecutor requires the 'async' gem. Add gem 'async' to your Gemfile."
|
|
152
|
-
end
|
|
153
|
-
end
|
|
154
|
-
end
|
|
155
|
-
end
|
|
156
|
-
end
|
|
@@ -1,467 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require_relative "../wait_result"
|
|
4
|
-
require_relative "../throttle_manager"
|
|
5
|
-
require_relative "../approval"
|
|
6
|
-
require_relative "../approval_store"
|
|
7
|
-
require_relative "../notifiers"
|
|
8
|
-
|
|
9
|
-
module RubyLLM
|
|
10
|
-
module Agents
|
|
11
|
-
class Workflow
|
|
12
|
-
module DSL
|
|
13
|
-
# Main executor for workflows using the refined DSL
|
|
14
|
-
#
|
|
15
|
-
# Handles the execution of steps in order, including sequential
|
|
16
|
-
# steps and parallel groups, with full support for routing,
|
|
17
|
-
# conditions, retries, and error handling.
|
|
18
|
-
#
|
|
19
|
-
# @api private
|
|
20
|
-
class Executor
|
|
21
|
-
attr_reader :workflow, :results, :errors, :status
|
|
22
|
-
|
|
23
|
-
# @param workflow [Workflow] The workflow instance
|
|
24
|
-
def initialize(workflow)
|
|
25
|
-
@workflow = workflow
|
|
26
|
-
@results = {}
|
|
27
|
-
@errors = {}
|
|
28
|
-
@status = "success"
|
|
29
|
-
@halted = false
|
|
30
|
-
@skip_next_step = false
|
|
31
|
-
@throttle_manager = ThrottleManager.new
|
|
32
|
-
@wait_results = {}
|
|
33
|
-
end
|
|
34
|
-
|
|
35
|
-
# Executes all workflow steps
|
|
36
|
-
#
|
|
37
|
-
# @yield [chunk] Streaming callback
|
|
38
|
-
# @return [Workflow::Result] The workflow result
|
|
39
|
-
def execute(&block)
|
|
40
|
-
@workflow_started_at = Time.current
|
|
41
|
-
|
|
42
|
-
# Validate input schema before execution
|
|
43
|
-
validate_input!
|
|
44
|
-
|
|
45
|
-
run_hooks(:before_workflow)
|
|
46
|
-
|
|
47
|
-
catch(:halt_workflow) do
|
|
48
|
-
execute_steps(&block)
|
|
49
|
-
end
|
|
50
|
-
|
|
51
|
-
run_hooks(:after_workflow)
|
|
52
|
-
|
|
53
|
-
build_result
|
|
54
|
-
rescue InputSchema::ValidationError
|
|
55
|
-
# Re-raise validation errors - these should not be caught
|
|
56
|
-
raise
|
|
57
|
-
rescue StandardError => e
|
|
58
|
-
@status = "error"
|
|
59
|
-
@errors[:workflow] = e
|
|
60
|
-
build_result(error: e)
|
|
61
|
-
end
|
|
62
|
-
|
|
63
|
-
private
|
|
64
|
-
|
|
65
|
-
# Validates input against the schema if defined
|
|
66
|
-
#
|
|
67
|
-
# This is called at the start of execution to fail fast on invalid input.
|
|
68
|
-
# Also populates the validated_input for later access.
|
|
69
|
-
#
|
|
70
|
-
# @raise [InputSchema::ValidationError] If input validation fails
|
|
71
|
-
def validate_input!
|
|
72
|
-
schema = workflow.class.input_schema
|
|
73
|
-
return unless schema
|
|
74
|
-
|
|
75
|
-
# This will raise ValidationError if input is invalid
|
|
76
|
-
validated = schema.validate!(workflow.options)
|
|
77
|
-
workflow.instance_variable_set(:@validated_input, OpenStruct.new(validated))
|
|
78
|
-
end
|
|
79
|
-
|
|
80
|
-
def execute_steps(&block)
|
|
81
|
-
previous_result = nil
|
|
82
|
-
|
|
83
|
-
workflow.class.step_order.each do |item|
|
|
84
|
-
break if @halted
|
|
85
|
-
|
|
86
|
-
# Handle skip_next from wait timeout
|
|
87
|
-
if @skip_next_step
|
|
88
|
-
@skip_next_step = false
|
|
89
|
-
next if item.is_a?(Symbol)
|
|
90
|
-
end
|
|
91
|
-
|
|
92
|
-
case item
|
|
93
|
-
when Symbol
|
|
94
|
-
previous_result = execute_single_step(item, previous_result, &block)
|
|
95
|
-
when ParallelGroup
|
|
96
|
-
previous_result = execute_parallel_group(item, &block)
|
|
97
|
-
when WaitConfig
|
|
98
|
-
wait_result = execute_wait_step(item)
|
|
99
|
-
@wait_results[item.object_id] = wait_result
|
|
100
|
-
handle_wait_result(wait_result)
|
|
101
|
-
end
|
|
102
|
-
end
|
|
103
|
-
end
|
|
104
|
-
|
|
105
|
-
def execute_single_step(step_name, previous_result, &block)
|
|
106
|
-
config = workflow.class.step_configs[step_name]
|
|
107
|
-
return previous_result unless config
|
|
108
|
-
|
|
109
|
-
# Apply throttling if configured
|
|
110
|
-
apply_throttle(step_name, config)
|
|
111
|
-
|
|
112
|
-
run_hooks(:before_step, step_name, workflow.step_results)
|
|
113
|
-
run_hooks(:on_step_start, step_name, config.resolve_input(workflow, previous_result))
|
|
114
|
-
|
|
115
|
-
started_at = Time.current
|
|
116
|
-
|
|
117
|
-
result = catch(:skip_step) do
|
|
118
|
-
executor = StepExecutor.new(workflow, config)
|
|
119
|
-
executor.execute(previous_result, &block)
|
|
120
|
-
end
|
|
121
|
-
|
|
122
|
-
# Handle skip_step catch
|
|
123
|
-
if result.is_a?(Hash) && result[:skipped]
|
|
124
|
-
result = if result[:default]
|
|
125
|
-
SimpleResult.new(content: result[:default], success: true)
|
|
126
|
-
else
|
|
127
|
-
SkippedResult.new(step_name, reason: result[:reason])
|
|
128
|
-
end
|
|
129
|
-
end
|
|
130
|
-
|
|
131
|
-
duration_ms = ((Time.current - started_at) * 1000).round
|
|
132
|
-
|
|
133
|
-
@results[step_name] = result
|
|
134
|
-
workflow.instance_variable_get(:@step_results)[step_name] = result
|
|
135
|
-
|
|
136
|
-
# Update status based on result
|
|
137
|
-
update_status_from_result(step_name, result, config)
|
|
138
|
-
|
|
139
|
-
run_hooks(:after_step, step_name, result, duration_ms)
|
|
140
|
-
run_hooks(:on_step_complete, step_name, result, duration_ms)
|
|
141
|
-
|
|
142
|
-
# Return nil on error for critical steps to prevent passing bad data
|
|
143
|
-
if result.respond_to?(:error?) && result.error? && config.critical?
|
|
144
|
-
@halted = true
|
|
145
|
-
return nil
|
|
146
|
-
end
|
|
147
|
-
|
|
148
|
-
result
|
|
149
|
-
rescue StandardError => e
|
|
150
|
-
handle_step_error(step_name, e, config)
|
|
151
|
-
end
|
|
152
|
-
|
|
153
|
-
def execute_parallel_group(group, &block)
|
|
154
|
-
results_mutex = Mutex.new
|
|
155
|
-
group_results = {}
|
|
156
|
-
group_errors = {}
|
|
157
|
-
|
|
158
|
-
# Determine pool size
|
|
159
|
-
pool_size = group.concurrency || group.step_names.size
|
|
160
|
-
pool = create_executor_pool(pool_size)
|
|
161
|
-
|
|
162
|
-
# Get the last result before this parallel group for input
|
|
163
|
-
last_sequential_step = workflow.class.step_order
|
|
164
|
-
.take_while { |item| item != group }
|
|
165
|
-
.select { |item| item.is_a?(Symbol) }
|
|
166
|
-
.last
|
|
167
|
-
previous_result = last_sequential_step ? @results[last_sequential_step] : nil
|
|
168
|
-
|
|
169
|
-
group.step_names.each do |step_name|
|
|
170
|
-
pool.post do
|
|
171
|
-
Thread.current.name = "parallel-#{step_name}"
|
|
172
|
-
|
|
173
|
-
begin
|
|
174
|
-
config = workflow.class.step_configs[step_name]
|
|
175
|
-
next unless config
|
|
176
|
-
|
|
177
|
-
executor = StepExecutor.new(workflow, config)
|
|
178
|
-
result = executor.execute(previous_result, &block)
|
|
179
|
-
|
|
180
|
-
results_mutex.synchronize do
|
|
181
|
-
group_results[step_name] = result
|
|
182
|
-
@results[step_name] = result
|
|
183
|
-
workflow.instance_variable_get(:@step_results)[step_name] = result
|
|
184
|
-
|
|
185
|
-
# Fail-fast handling
|
|
186
|
-
if group.fail_fast? && result.respond_to?(:error?) && result.error? && config.critical?
|
|
187
|
-
pool.abort! if pool.respond_to?(:abort!)
|
|
188
|
-
end
|
|
189
|
-
end
|
|
190
|
-
rescue StandardError => e
|
|
191
|
-
results_mutex.synchronize do
|
|
192
|
-
group_errors[step_name] = e
|
|
193
|
-
@errors[step_name] = e
|
|
194
|
-
|
|
195
|
-
if group.fail_fast?
|
|
196
|
-
pool.abort! if pool.respond_to?(:abort!)
|
|
197
|
-
end
|
|
198
|
-
end
|
|
199
|
-
end
|
|
200
|
-
end
|
|
201
|
-
end
|
|
202
|
-
|
|
203
|
-
pool.wait_for_completion
|
|
204
|
-
pool.shutdown
|
|
205
|
-
|
|
206
|
-
# Update overall status
|
|
207
|
-
update_parallel_status(group, group_results, group_errors)
|
|
208
|
-
|
|
209
|
-
# Return combined results as a hash-like object
|
|
210
|
-
ParallelGroupResult.new(group.name, group_results)
|
|
211
|
-
end
|
|
212
|
-
|
|
213
|
-
def create_executor_pool(size)
|
|
214
|
-
config = RubyLLM::Agents.configuration
|
|
215
|
-
|
|
216
|
-
if config.respond_to?(:async_context?) && config.async_context?
|
|
217
|
-
AsyncExecutor.new(max_concurrent: size)
|
|
218
|
-
else
|
|
219
|
-
ThreadPool.new(size: size)
|
|
220
|
-
end
|
|
221
|
-
end
|
|
222
|
-
|
|
223
|
-
# Executes a wait step
|
|
224
|
-
#
|
|
225
|
-
# @param wait_config [WaitConfig] The wait configuration
|
|
226
|
-
# @return [WaitResult] The wait result
|
|
227
|
-
def execute_wait_step(wait_config)
|
|
228
|
-
executor = WaitExecutor.new(wait_config, workflow)
|
|
229
|
-
executor.execute
|
|
230
|
-
rescue StandardError => e
|
|
231
|
-
# Return a failed result instead of crashing
|
|
232
|
-
Workflow::WaitResult.timeout(
|
|
233
|
-
wait_config.type,
|
|
234
|
-
0,
|
|
235
|
-
:fail,
|
|
236
|
-
error: "#{e.class}: #{e.message}"
|
|
237
|
-
)
|
|
238
|
-
end
|
|
239
|
-
|
|
240
|
-
# Handles the result of a wait step
|
|
241
|
-
#
|
|
242
|
-
# @param wait_result [WaitResult] The wait result
|
|
243
|
-
# @return [void]
|
|
244
|
-
def handle_wait_result(wait_result)
|
|
245
|
-
if wait_result.timeout? && wait_result.timeout_action == :fail
|
|
246
|
-
@status = "error"
|
|
247
|
-
@halted = true
|
|
248
|
-
@errors[:wait] = "Wait timed out: #{wait_result.type}"
|
|
249
|
-
elsif wait_result.rejected?
|
|
250
|
-
@status = "error"
|
|
251
|
-
@halted = true
|
|
252
|
-
@errors[:wait] = "Approval rejected: #{wait_result.rejection_reason}"
|
|
253
|
-
elsif wait_result.should_skip_next?
|
|
254
|
-
@skip_next_step = true
|
|
255
|
-
end
|
|
256
|
-
end
|
|
257
|
-
|
|
258
|
-
# Applies throttling for a step if configured
|
|
259
|
-
#
|
|
260
|
-
# @param step_name [Symbol] The step name
|
|
261
|
-
# @param config [StepConfig] The step configuration
|
|
262
|
-
# @return [void]
|
|
263
|
-
def apply_throttle(step_name, config)
|
|
264
|
-
return unless config.throttled?
|
|
265
|
-
|
|
266
|
-
if config.throttle
|
|
267
|
-
@throttle_manager.throttle("step:#{step_name}", config.throttle)
|
|
268
|
-
elsif config.rate_limit
|
|
269
|
-
@throttle_manager.rate_limit(
|
|
270
|
-
"step:#{step_name}",
|
|
271
|
-
calls: config.rate_limit[:calls],
|
|
272
|
-
per: config.rate_limit[:per]
|
|
273
|
-
)
|
|
274
|
-
end
|
|
275
|
-
end
|
|
276
|
-
|
|
277
|
-
def handle_step_error(step_name, error, config)
|
|
278
|
-
@errors[step_name] = error
|
|
279
|
-
|
|
280
|
-
run_hooks(:on_step_error, step_name, error)
|
|
281
|
-
run_hooks(:on_step_failure, step_name, error, workflow.step_results)
|
|
282
|
-
|
|
283
|
-
# Build error result
|
|
284
|
-
error_result = Pipeline::ErrorResult.new(
|
|
285
|
-
step_name: step_name,
|
|
286
|
-
error_class: error.class.name,
|
|
287
|
-
error_message: error.message
|
|
288
|
-
)
|
|
289
|
-
|
|
290
|
-
@results[step_name] = error_result
|
|
291
|
-
workflow.instance_variable_get(:@step_results)[step_name] = error_result
|
|
292
|
-
|
|
293
|
-
if config.optional?
|
|
294
|
-
@status = "partial" if @status == "success"
|
|
295
|
-
config.default_value ? SimpleResult.new(content: config.default_value, success: true) : nil
|
|
296
|
-
else
|
|
297
|
-
@status = "error"
|
|
298
|
-
@halted = true
|
|
299
|
-
nil
|
|
300
|
-
end
|
|
301
|
-
end
|
|
302
|
-
|
|
303
|
-
def update_status_from_result(step_name, result, config)
|
|
304
|
-
return unless result.respond_to?(:error?) && result.error?
|
|
305
|
-
|
|
306
|
-
if config.optional?
|
|
307
|
-
@status = "partial" if @status == "success"
|
|
308
|
-
else
|
|
309
|
-
@status = "error"
|
|
310
|
-
end
|
|
311
|
-
end
|
|
312
|
-
|
|
313
|
-
def update_parallel_status(group, group_results, group_errors)
|
|
314
|
-
# Check for errors
|
|
315
|
-
group.step_names.each do |step_name|
|
|
316
|
-
config = workflow.class.step_configs[step_name]
|
|
317
|
-
|
|
318
|
-
if group_errors[step_name]
|
|
319
|
-
if config&.optional?
|
|
320
|
-
@status = "partial" if @status == "success"
|
|
321
|
-
else
|
|
322
|
-
@status = "error"
|
|
323
|
-
end
|
|
324
|
-
elsif group_results[step_name]&.respond_to?(:error?) && group_results[step_name].error?
|
|
325
|
-
if config&.optional?
|
|
326
|
-
@status = "partial" if @status == "success"
|
|
327
|
-
else
|
|
328
|
-
@status = "error"
|
|
329
|
-
end
|
|
330
|
-
end
|
|
331
|
-
end
|
|
332
|
-
end
|
|
333
|
-
|
|
334
|
-
def run_hooks(hook_name, *args)
|
|
335
|
-
workflow.send(:run_hooks, hook_name, *args)
|
|
336
|
-
end
|
|
337
|
-
|
|
338
|
-
def build_result(error: nil)
|
|
339
|
-
# Get final content from last successful step
|
|
340
|
-
final_content = extract_final_content
|
|
341
|
-
|
|
342
|
-
# Validate output if schema defined
|
|
343
|
-
if workflow.class.output_schema && final_content
|
|
344
|
-
begin
|
|
345
|
-
workflow.class.output_schema.validate!(final_content)
|
|
346
|
-
rescue InputSchema::ValidationError => e
|
|
347
|
-
@errors[:output_validation] = e
|
|
348
|
-
@status = "error" if @status == "success"
|
|
349
|
-
end
|
|
350
|
-
end
|
|
351
|
-
|
|
352
|
-
Workflow::Result.new(
|
|
353
|
-
content: final_content,
|
|
354
|
-
workflow_type: workflow.class.name,
|
|
355
|
-
workflow_id: workflow.workflow_id,
|
|
356
|
-
steps: @results,
|
|
357
|
-
errors: @errors,
|
|
358
|
-
status: @status,
|
|
359
|
-
error_class: error&.class&.name,
|
|
360
|
-
error_message: error&.message,
|
|
361
|
-
started_at: @workflow_started_at,
|
|
362
|
-
completed_at: Time.current,
|
|
363
|
-
duration_ms: (((Time.current - @workflow_started_at) * 1000).round if @workflow_started_at)
|
|
364
|
-
)
|
|
365
|
-
end
|
|
366
|
-
|
|
367
|
-
def extract_final_content
|
|
368
|
-
# Find the last successful result
|
|
369
|
-
workflow.class.step_order.reverse.each do |item|
|
|
370
|
-
case item
|
|
371
|
-
when Symbol
|
|
372
|
-
result = @results[item]
|
|
373
|
-
next if result.nil?
|
|
374
|
-
next if result.respond_to?(:skipped?) && result.skipped?
|
|
375
|
-
next if result.respond_to?(:error?) && result.error?
|
|
376
|
-
return result.content if result.respond_to?(:content)
|
|
377
|
-
when ParallelGroup
|
|
378
|
-
# For parallel groups, return the combined content
|
|
379
|
-
group_content = {}
|
|
380
|
-
item.step_names.each do |step_name|
|
|
381
|
-
result = @results[step_name]
|
|
382
|
-
next if result.nil? || (result.respond_to?(:error?) && result.error?)
|
|
383
|
-
group_content[step_name] = result.respond_to?(:content) ? result.content : result
|
|
384
|
-
end
|
|
385
|
-
return group_content if group_content.any?
|
|
386
|
-
when WaitConfig
|
|
387
|
-
# Wait steps don't contribute content, skip them
|
|
388
|
-
next
|
|
389
|
-
end
|
|
390
|
-
end
|
|
391
|
-
|
|
392
|
-
nil
|
|
393
|
-
end
|
|
394
|
-
end
|
|
395
|
-
|
|
396
|
-
# Result wrapper for parallel group execution
|
|
397
|
-
#
|
|
398
|
-
# Provides access to individual step results within a parallel group.
|
|
399
|
-
#
|
|
400
|
-
# @api private
|
|
401
|
-
class ParallelGroupResult
|
|
402
|
-
attr_reader :name, :results
|
|
403
|
-
|
|
404
|
-
def initialize(name, results)
|
|
405
|
-
@name = name
|
|
406
|
-
@results = results
|
|
407
|
-
end
|
|
408
|
-
|
|
409
|
-
def content
|
|
410
|
-
@results.transform_values { |r| r&.content }
|
|
411
|
-
end
|
|
412
|
-
|
|
413
|
-
def [](key)
|
|
414
|
-
@results[key]
|
|
415
|
-
end
|
|
416
|
-
|
|
417
|
-
def success?
|
|
418
|
-
@results.values.all? { |r| r.nil? || !r.respond_to?(:error?) || !r.error? }
|
|
419
|
-
end
|
|
420
|
-
|
|
421
|
-
def error?
|
|
422
|
-
!success?
|
|
423
|
-
end
|
|
424
|
-
|
|
425
|
-
def to_h
|
|
426
|
-
content
|
|
427
|
-
end
|
|
428
|
-
|
|
429
|
-
def method_missing(name, *args, &block)
|
|
430
|
-
if @results.key?(name)
|
|
431
|
-
@results[name]
|
|
432
|
-
elsif content.key?(name)
|
|
433
|
-
content[name]
|
|
434
|
-
else
|
|
435
|
-
super
|
|
436
|
-
end
|
|
437
|
-
end
|
|
438
|
-
|
|
439
|
-
def respond_to_missing?(name, include_private = false)
|
|
440
|
-
@results.key?(name) || content.key?(name) || super
|
|
441
|
-
end
|
|
442
|
-
|
|
443
|
-
# Token/cost aggregation
|
|
444
|
-
def input_tokens
|
|
445
|
-
@results.values.compact.sum { |r| r.respond_to?(:input_tokens) ? r.input_tokens : 0 }
|
|
446
|
-
end
|
|
447
|
-
|
|
448
|
-
def output_tokens
|
|
449
|
-
@results.values.compact.sum { |r| r.respond_to?(:output_tokens) ? r.output_tokens : 0 }
|
|
450
|
-
end
|
|
451
|
-
|
|
452
|
-
def total_tokens
|
|
453
|
-
input_tokens + output_tokens
|
|
454
|
-
end
|
|
455
|
-
|
|
456
|
-
def cached_tokens
|
|
457
|
-
@results.values.compact.sum { |r| r.respond_to?(:cached_tokens) ? r.cached_tokens : 0 }
|
|
458
|
-
end
|
|
459
|
-
|
|
460
|
-
def total_cost
|
|
461
|
-
@results.values.compact.sum { |r| r.respond_to?(:total_cost) ? r.total_cost : 0.0 }
|
|
462
|
-
end
|
|
463
|
-
end
|
|
464
|
-
end
|
|
465
|
-
end
|
|
466
|
-
end
|
|
467
|
-
end
|