ruby_llm-agents 1.0.0.beta.1 → 1.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/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/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/restructure_generator.rb +2 -2
- data/lib/generators/ruby_llm_agents/speaker_generator.rb +8 -39
- 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/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/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/ruby_llm/agents/core/configuration.rb +55 -43
- 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/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 +37 -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,576 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "ostruct"
|
|
4
|
+
require_relative "dsl/step_config"
|
|
5
|
+
require_relative "dsl/route_builder"
|
|
6
|
+
require_relative "dsl/parallel_group"
|
|
7
|
+
require_relative "dsl/input_schema"
|
|
8
|
+
require_relative "dsl/step_executor"
|
|
9
|
+
require_relative "dsl/iteration_executor"
|
|
10
|
+
require_relative "dsl/wait_config"
|
|
11
|
+
require_relative "dsl/wait_executor"
|
|
12
|
+
require_relative "dsl/schedule_helpers"
|
|
13
|
+
|
|
14
|
+
module RubyLLM
|
|
15
|
+
module Agents
|
|
16
|
+
class Workflow
|
|
17
|
+
# Refined DSL for declarative workflow definition
|
|
18
|
+
#
|
|
19
|
+
# This module provides a clean, expressive syntax for defining workflows
|
|
20
|
+
# with minimal boilerplate for common patterns while maintaining full
|
|
21
|
+
# flexibility for complex scenarios.
|
|
22
|
+
#
|
|
23
|
+
# @example Minimal workflow
|
|
24
|
+
# class SimpleWorkflow < RubyLLM::Agents::Workflow
|
|
25
|
+
# step :fetch, FetcherAgent
|
|
26
|
+
# step :process, ProcessorAgent
|
|
27
|
+
# step :save, SaverAgent
|
|
28
|
+
# end
|
|
29
|
+
#
|
|
30
|
+
# @example Full-featured workflow
|
|
31
|
+
# class OrderWorkflow < RubyLLM::Agents::Workflow
|
|
32
|
+
# description "Process customer orders end-to-end"
|
|
33
|
+
#
|
|
34
|
+
# input do
|
|
35
|
+
# required :order_id, String
|
|
36
|
+
# optional :priority, String, default: "normal"
|
|
37
|
+
# end
|
|
38
|
+
#
|
|
39
|
+
# step :fetch, FetcherAgent, timeout: 1.minute
|
|
40
|
+
# step :validate, ValidatorAgent
|
|
41
|
+
#
|
|
42
|
+
# step :process, on: -> { validate.tier } do |route|
|
|
43
|
+
# route.premium PremiumAgent
|
|
44
|
+
# route.standard StandardAgent
|
|
45
|
+
# route.default DefaultAgent
|
|
46
|
+
# end
|
|
47
|
+
#
|
|
48
|
+
# parallel do
|
|
49
|
+
# step :analyze, AnalyzerAgent
|
|
50
|
+
# step :summarize, SummarizerAgent
|
|
51
|
+
# end
|
|
52
|
+
#
|
|
53
|
+
# step :notify, NotifierAgent, if: :should_notify?
|
|
54
|
+
#
|
|
55
|
+
# private
|
|
56
|
+
#
|
|
57
|
+
# def should_notify?
|
|
58
|
+
# input.callback_url.present?
|
|
59
|
+
# end
|
|
60
|
+
# end
|
|
61
|
+
#
|
|
62
|
+
# @api public
|
|
63
|
+
module DSL
|
|
64
|
+
def self.included(base)
|
|
65
|
+
base.extend(ClassMethods)
|
|
66
|
+
base.include(InstanceMethods)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Class-level DSL methods
|
|
70
|
+
module ClassMethods
|
|
71
|
+
# Returns the ordered list of steps/groups
|
|
72
|
+
#
|
|
73
|
+
# @return [Array<Symbol, ParallelGroup>]
|
|
74
|
+
def step_order
|
|
75
|
+
@step_order ||= []
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Returns step configurations
|
|
79
|
+
#
|
|
80
|
+
# @return [Hash<Symbol, StepConfig>]
|
|
81
|
+
def step_configs
|
|
82
|
+
@step_configs ||= {}
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Returns parallel groups
|
|
86
|
+
#
|
|
87
|
+
# @return [Array<ParallelGroup>]
|
|
88
|
+
def parallel_groups
|
|
89
|
+
@parallel_groups ||= []
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Returns wait step configurations
|
|
93
|
+
#
|
|
94
|
+
# @return [Array<WaitConfig>]
|
|
95
|
+
def wait_configs
|
|
96
|
+
@wait_configs ||= []
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Returns the input schema
|
|
100
|
+
#
|
|
101
|
+
# @return [InputSchema, nil]
|
|
102
|
+
def input_schema
|
|
103
|
+
@input_schema
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Returns the output schema
|
|
107
|
+
#
|
|
108
|
+
# @return [OutputSchema, nil]
|
|
109
|
+
def output_schema
|
|
110
|
+
@output_schema
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# Inherits DSL configuration from parent class
|
|
114
|
+
def inherited(subclass)
|
|
115
|
+
super
|
|
116
|
+
subclass.instance_variable_set(:@step_order, step_order.dup)
|
|
117
|
+
subclass.instance_variable_set(:@step_configs, step_configs.dup)
|
|
118
|
+
subclass.instance_variable_set(:@parallel_groups, parallel_groups.dup)
|
|
119
|
+
subclass.instance_variable_set(:@wait_configs, wait_configs.dup)
|
|
120
|
+
subclass.instance_variable_set(:@input_schema, input_schema&.dup)
|
|
121
|
+
subclass.instance_variable_set(:@output_schema, output_schema&.dup)
|
|
122
|
+
subclass.instance_variable_set(:@lifecycle_hooks, @lifecycle_hooks&.dup || {})
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# Defines a workflow step
|
|
126
|
+
#
|
|
127
|
+
# @param name [Symbol] Step identifier
|
|
128
|
+
# @param agent [Class, nil] Agent class to execute (optional if using block)
|
|
129
|
+
# @param desc [String, nil] Human-readable description
|
|
130
|
+
# @param options [Hash] Step options (timeout, retry, if, unless, etc.)
|
|
131
|
+
# @yield [route] Block for routing or custom logic
|
|
132
|
+
# @return [void]
|
|
133
|
+
#
|
|
134
|
+
# @example Minimal step
|
|
135
|
+
# step :validate, ValidatorAgent
|
|
136
|
+
#
|
|
137
|
+
# @example With options
|
|
138
|
+
# step :fetch, FetcherAgent, timeout: 30.seconds, retry: 3
|
|
139
|
+
#
|
|
140
|
+
# @example With routing
|
|
141
|
+
# step :process, on: -> { classify.type } do |r|
|
|
142
|
+
# r.typeA AgentA
|
|
143
|
+
# r.typeB AgentB
|
|
144
|
+
# r.default DefaultAgent
|
|
145
|
+
# end
|
|
146
|
+
#
|
|
147
|
+
# @example With custom block
|
|
148
|
+
# step :custom do
|
|
149
|
+
# skip! "No data" if input.data.empty?
|
|
150
|
+
# agent CustomAgent, data: transform(input.data)
|
|
151
|
+
# end
|
|
152
|
+
def step(name, agent = nil, desc = nil, **options, &block)
|
|
153
|
+
# Handle positional description
|
|
154
|
+
description = desc.is_a?(String) ? desc : nil
|
|
155
|
+
if desc.is_a?(Hash)
|
|
156
|
+
options = desc.merge(options)
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
config = StepConfig.new(
|
|
160
|
+
name: name,
|
|
161
|
+
agent: agent,
|
|
162
|
+
description: description,
|
|
163
|
+
options: options,
|
|
164
|
+
block: block
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
step_configs[name] = config
|
|
168
|
+
|
|
169
|
+
# Add to order if not in a parallel block
|
|
170
|
+
unless @_defining_parallel
|
|
171
|
+
step_order << name
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
# Defines a group of steps that execute in parallel
|
|
176
|
+
#
|
|
177
|
+
# @param name [Symbol, nil] Optional name for the group
|
|
178
|
+
# @param options [Hash] Group options (fail_fast, concurrency, timeout)
|
|
179
|
+
# @yield Block defining the parallel steps
|
|
180
|
+
# @return [void]
|
|
181
|
+
#
|
|
182
|
+
# @example Unnamed parallel group
|
|
183
|
+
# parallel do
|
|
184
|
+
# step :sentiment, SentimentAgent
|
|
185
|
+
# step :keywords, KeywordAgent
|
|
186
|
+
# end
|
|
187
|
+
#
|
|
188
|
+
# @example Named parallel group
|
|
189
|
+
# parallel :analysis do
|
|
190
|
+
# step :sentiment, SentimentAgent
|
|
191
|
+
# step :keywords, KeywordAgent
|
|
192
|
+
# end
|
|
193
|
+
def parallel(name = nil, **options, &block)
|
|
194
|
+
@_defining_parallel = true
|
|
195
|
+
previous_step_count = step_configs.size
|
|
196
|
+
|
|
197
|
+
# Execute the block to collect step definitions
|
|
198
|
+
instance_eval(&block)
|
|
199
|
+
|
|
200
|
+
# Find newly added steps
|
|
201
|
+
new_steps = step_configs.keys.last(step_configs.size - previous_step_count)
|
|
202
|
+
|
|
203
|
+
group = ParallelGroup.new(
|
|
204
|
+
name: name,
|
|
205
|
+
step_names: new_steps,
|
|
206
|
+
options: options
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
parallel_groups << group
|
|
210
|
+
step_order << group
|
|
211
|
+
|
|
212
|
+
@_defining_parallel = false
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
# Defines a simple delay wait step
|
|
216
|
+
#
|
|
217
|
+
# @param duration [ActiveSupport::Duration, Integer, Float] Duration to wait
|
|
218
|
+
# @param options [Hash] Wait options (if:, unless:)
|
|
219
|
+
# @return [void]
|
|
220
|
+
#
|
|
221
|
+
# @example Simple delay
|
|
222
|
+
# wait 5.seconds
|
|
223
|
+
#
|
|
224
|
+
# @example Conditional delay
|
|
225
|
+
# wait 5.seconds, if: :needs_cooldown?
|
|
226
|
+
def wait(duration, **options)
|
|
227
|
+
config = WaitConfig.new(type: :delay, duration: duration, **options)
|
|
228
|
+
wait_configs << config
|
|
229
|
+
step_order << config
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
# Defines a conditional wait step that polls until a condition is met
|
|
233
|
+
#
|
|
234
|
+
# @param condition [Proc, nil] Lambda that returns true when ready to proceed
|
|
235
|
+
# @param time [Proc, Time, nil] Time to wait until (for scheduled waits)
|
|
236
|
+
# @param options [Hash] Wait options (poll_interval:, timeout:, on_timeout:, backoff:)
|
|
237
|
+
# @yield Block as condition (alternative to condition param)
|
|
238
|
+
# @return [void]
|
|
239
|
+
#
|
|
240
|
+
# @example Wait until condition
|
|
241
|
+
# wait_until -> { payment.confirmed? }, poll_interval: 5.seconds, timeout: 10.minutes
|
|
242
|
+
#
|
|
243
|
+
# @example With block
|
|
244
|
+
# wait_until(poll_interval: 1.second) { order.ready? }
|
|
245
|
+
#
|
|
246
|
+
# @example Wait until specific time
|
|
247
|
+
# wait_until time: -> { next_weekday_at(9, 0) }
|
|
248
|
+
def wait_until(condition = nil, time: nil, **options, &block)
|
|
249
|
+
condition ||= block
|
|
250
|
+
|
|
251
|
+
if time
|
|
252
|
+
config = WaitConfig.new(type: :schedule, condition: time, **options)
|
|
253
|
+
else
|
|
254
|
+
config = WaitConfig.new(type: :until, condition: condition, **options)
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
wait_configs << config
|
|
258
|
+
step_order << config
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
# Defines a human-in-the-loop approval wait step
|
|
262
|
+
#
|
|
263
|
+
# @param name [Symbol] Identifier for the approval point
|
|
264
|
+
# @param options [Hash] Approval options (notify:, message:, timeout:, approvers:, etc.)
|
|
265
|
+
# @return [void]
|
|
266
|
+
#
|
|
267
|
+
# @example Simple approval
|
|
268
|
+
# wait_for :manager_approval
|
|
269
|
+
#
|
|
270
|
+
# @example With notifications and timeout
|
|
271
|
+
# wait_for :review,
|
|
272
|
+
# notify: [:email, :slack],
|
|
273
|
+
# message: -> { "Please review: #{draft.title}" },
|
|
274
|
+
# timeout: 24.hours,
|
|
275
|
+
# reminder_after: 4.hours
|
|
276
|
+
def wait_for(name, **options)
|
|
277
|
+
config = WaitConfig.new(type: :approval, name: name, **options)
|
|
278
|
+
wait_configs << config
|
|
279
|
+
step_order << config
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
# Defines the input schema
|
|
283
|
+
#
|
|
284
|
+
# @yield Block defining required and optional fields
|
|
285
|
+
# @return [void]
|
|
286
|
+
#
|
|
287
|
+
# @example
|
|
288
|
+
# input do
|
|
289
|
+
# required :order_id, String
|
|
290
|
+
# optional :priority, String, default: "normal"
|
|
291
|
+
# end
|
|
292
|
+
def input(&block)
|
|
293
|
+
@input_schema = InputSchema.new
|
|
294
|
+
@input_schema.instance_eval(&block)
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
# Defines the output schema
|
|
298
|
+
#
|
|
299
|
+
# @yield Block defining required and optional fields
|
|
300
|
+
# @return [void]
|
|
301
|
+
def output(&block)
|
|
302
|
+
@output_schema = OutputSchema.new
|
|
303
|
+
@output_schema.instance_eval(&block)
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
# Registers a lifecycle hook
|
|
307
|
+
#
|
|
308
|
+
# @param hook_name [Symbol] Hook type
|
|
309
|
+
# @param step_name [Symbol, nil] Specific step (nil for all)
|
|
310
|
+
# @param method_name [Symbol, nil] Method to call
|
|
311
|
+
# @yield Block to execute
|
|
312
|
+
# @return [void]
|
|
313
|
+
def register_hook(hook_name, step_name = nil, method_name = nil, &block)
|
|
314
|
+
@lifecycle_hooks ||= {}
|
|
315
|
+
@lifecycle_hooks[hook_name] ||= []
|
|
316
|
+
@lifecycle_hooks[hook_name] << {
|
|
317
|
+
step: step_name,
|
|
318
|
+
method: method_name,
|
|
319
|
+
block: block
|
|
320
|
+
}
|
|
321
|
+
end
|
|
322
|
+
|
|
323
|
+
# Hooks that run before the workflow starts
|
|
324
|
+
def before_workflow(method_name = nil, &block)
|
|
325
|
+
register_hook(:before_workflow, nil, method_name, &block)
|
|
326
|
+
end
|
|
327
|
+
|
|
328
|
+
# Hooks that run after the workflow completes
|
|
329
|
+
def after_workflow(method_name = nil, &block)
|
|
330
|
+
register_hook(:after_workflow, nil, method_name, &block)
|
|
331
|
+
end
|
|
332
|
+
|
|
333
|
+
# Hooks that run before each step
|
|
334
|
+
def before_step(step_name = nil, method_name = nil, &block)
|
|
335
|
+
register_hook(:before_step, step_name, method_name, &block)
|
|
336
|
+
end
|
|
337
|
+
|
|
338
|
+
# Hooks that run after each step
|
|
339
|
+
def after_step(step_name = nil, method_name = nil, &block)
|
|
340
|
+
register_hook(:after_step, step_name, method_name, &block)
|
|
341
|
+
end
|
|
342
|
+
|
|
343
|
+
# Hooks that run when a step fails
|
|
344
|
+
def on_step_failure(step_name = nil, method_name = nil, &block)
|
|
345
|
+
register_hook(:on_step_failure, step_name, method_name, &block)
|
|
346
|
+
end
|
|
347
|
+
|
|
348
|
+
# Hooks that run when a step starts
|
|
349
|
+
def on_step_start(method_name = nil, &block)
|
|
350
|
+
register_hook(:on_step_start, nil, method_name, &block)
|
|
351
|
+
end
|
|
352
|
+
|
|
353
|
+
# Hooks that run when a step completes
|
|
354
|
+
def on_step_complete(method_name = nil, &block)
|
|
355
|
+
register_hook(:on_step_complete, nil, method_name, &block)
|
|
356
|
+
end
|
|
357
|
+
|
|
358
|
+
# Hooks that run when a step errors
|
|
359
|
+
def on_step_error(method_name = nil, &block)
|
|
360
|
+
register_hook(:on_step_error, nil, method_name, &block)
|
|
361
|
+
end
|
|
362
|
+
|
|
363
|
+
# Returns lifecycle hooks
|
|
364
|
+
#
|
|
365
|
+
# @return [Hash]
|
|
366
|
+
def lifecycle_hooks
|
|
367
|
+
@lifecycle_hooks ||= {}
|
|
368
|
+
end
|
|
369
|
+
|
|
370
|
+
# Returns step metadata for UI display
|
|
371
|
+
#
|
|
372
|
+
# @return [Array<Hash>]
|
|
373
|
+
def step_metadata
|
|
374
|
+
step_order.flat_map do |item|
|
|
375
|
+
case item
|
|
376
|
+
when Symbol
|
|
377
|
+
config = step_configs[item]
|
|
378
|
+
[{
|
|
379
|
+
name: item,
|
|
380
|
+
agent: config.agent&.name,
|
|
381
|
+
description: config.description,
|
|
382
|
+
ui_label: config.ui_label || item.to_s.humanize,
|
|
383
|
+
optional: config.optional?,
|
|
384
|
+
timeout: config.timeout,
|
|
385
|
+
routing: config.routing?,
|
|
386
|
+
parallel: false,
|
|
387
|
+
workflow: config.workflow?,
|
|
388
|
+
iteration: config.iteration?,
|
|
389
|
+
iteration_concurrency: config.iteration_concurrency,
|
|
390
|
+
throttle: config.throttle,
|
|
391
|
+
rate_limit: config.rate_limit
|
|
392
|
+
}.compact]
|
|
393
|
+
when ParallelGroup
|
|
394
|
+
item.step_names.map do |step_name|
|
|
395
|
+
config = step_configs[step_name]
|
|
396
|
+
{
|
|
397
|
+
name: step_name,
|
|
398
|
+
agent: config.agent&.name,
|
|
399
|
+
description: config.description,
|
|
400
|
+
ui_label: config.ui_label || step_name.to_s.humanize,
|
|
401
|
+
optional: config.optional?,
|
|
402
|
+
timeout: config.timeout,
|
|
403
|
+
routing: config.routing?,
|
|
404
|
+
parallel: true,
|
|
405
|
+
parallel_group: item.name,
|
|
406
|
+
workflow: config.workflow?,
|
|
407
|
+
iteration: config.iteration?,
|
|
408
|
+
iteration_concurrency: config.iteration_concurrency,
|
|
409
|
+
throttle: config.throttle,
|
|
410
|
+
rate_limit: config.rate_limit
|
|
411
|
+
}.compact
|
|
412
|
+
end
|
|
413
|
+
when WaitConfig
|
|
414
|
+
[{
|
|
415
|
+
name: item.name || "wait_#{item.type}",
|
|
416
|
+
type: :wait,
|
|
417
|
+
wait_type: item.type,
|
|
418
|
+
ui_label: item.ui_label,
|
|
419
|
+
timeout: item.timeout,
|
|
420
|
+
parallel: false,
|
|
421
|
+
duration: item.duration,
|
|
422
|
+
poll_interval: item.poll_interval,
|
|
423
|
+
on_timeout: item.on_timeout,
|
|
424
|
+
notify: item.notify_channels,
|
|
425
|
+
approvers: item.approvers
|
|
426
|
+
}.compact]
|
|
427
|
+
end
|
|
428
|
+
end
|
|
429
|
+
end
|
|
430
|
+
|
|
431
|
+
# Returns the total number of steps
|
|
432
|
+
#
|
|
433
|
+
# @return [Integer]
|
|
434
|
+
def total_steps
|
|
435
|
+
step_configs.size
|
|
436
|
+
end
|
|
437
|
+
|
|
438
|
+
# Validates workflow configuration
|
|
439
|
+
#
|
|
440
|
+
# @return [Array<String>] Validation errors
|
|
441
|
+
def validate_configuration
|
|
442
|
+
errors = []
|
|
443
|
+
|
|
444
|
+
step_configs.each do |name, config|
|
|
445
|
+
if config.agent.nil? && !config.custom_block? && !config.routing?
|
|
446
|
+
errors << "Step :#{name} has no agent defined"
|
|
447
|
+
end
|
|
448
|
+
|
|
449
|
+
if config.routing?
|
|
450
|
+
builder = RouteBuilder.new
|
|
451
|
+
config.block.call(builder)
|
|
452
|
+
if builder.routes.empty? && builder.default.nil?
|
|
453
|
+
errors << "Step :#{name} has no routes defined"
|
|
454
|
+
end
|
|
455
|
+
end
|
|
456
|
+
end
|
|
457
|
+
|
|
458
|
+
errors
|
|
459
|
+
end
|
|
460
|
+
end
|
|
461
|
+
|
|
462
|
+
# Instance-level DSL methods
|
|
463
|
+
module InstanceMethods
|
|
464
|
+
# Returns the validated input
|
|
465
|
+
#
|
|
466
|
+
# @return [OpenStruct] Input with accessor methods
|
|
467
|
+
def input
|
|
468
|
+
@validated_input ||= begin
|
|
469
|
+
schema = self.class.input_schema
|
|
470
|
+
validated = schema ? schema.validate!(options) : options
|
|
471
|
+
OpenStruct.new(validated)
|
|
472
|
+
end
|
|
473
|
+
end
|
|
474
|
+
|
|
475
|
+
# Returns a step result by name
|
|
476
|
+
#
|
|
477
|
+
# @param name [Symbol] Step name
|
|
478
|
+
# @return [Result, nil]
|
|
479
|
+
def step_result(name)
|
|
480
|
+
@step_results[name]
|
|
481
|
+
end
|
|
482
|
+
|
|
483
|
+
# Returns all step results
|
|
484
|
+
#
|
|
485
|
+
# @return [Hash<Symbol, Result>]
|
|
486
|
+
attr_reader :step_results
|
|
487
|
+
|
|
488
|
+
# Provides dynamic access to step results
|
|
489
|
+
#
|
|
490
|
+
# Allows accessing step results as methods:
|
|
491
|
+
# validate.content # Returns the :validate step result's content
|
|
492
|
+
#
|
|
493
|
+
def method_missing(name, *args, &block)
|
|
494
|
+
if @step_results&.key?(name)
|
|
495
|
+
result = @step_results[name]
|
|
496
|
+
# Return a proxy that allows accessing content
|
|
497
|
+
StepResultProxy.new(result)
|
|
498
|
+
else
|
|
499
|
+
super
|
|
500
|
+
end
|
|
501
|
+
end
|
|
502
|
+
|
|
503
|
+
def respond_to_missing?(name, include_private = false)
|
|
504
|
+
@step_results&.key?(name) || super
|
|
505
|
+
end
|
|
506
|
+
|
|
507
|
+
protected
|
|
508
|
+
|
|
509
|
+
# Executes lifecycle hooks
|
|
510
|
+
#
|
|
511
|
+
# @param hook_name [Symbol] Hook type
|
|
512
|
+
# @param step_name [Symbol, nil] Current step name
|
|
513
|
+
# @param args [Array] Arguments to pass to hooks
|
|
514
|
+
def run_hooks(hook_name, step_name = nil, *args)
|
|
515
|
+
hooks = self.class.lifecycle_hooks[hook_name] || []
|
|
516
|
+
|
|
517
|
+
hooks.each do |hook|
|
|
518
|
+
# Skip if hook is for a specific step and this isn't it
|
|
519
|
+
next if hook[:step] && hook[:step] != step_name
|
|
520
|
+
|
|
521
|
+
if hook[:method]
|
|
522
|
+
send(hook[:method], *args)
|
|
523
|
+
elsif hook[:block]
|
|
524
|
+
instance_exec(*args, &hook[:block])
|
|
525
|
+
end
|
|
526
|
+
end
|
|
527
|
+
end
|
|
528
|
+
end
|
|
529
|
+
|
|
530
|
+
# Proxy for accessing step results
|
|
531
|
+
#
|
|
532
|
+
# Provides convenient access to step result content and methods.
|
|
533
|
+
#
|
|
534
|
+
# @api private
|
|
535
|
+
class StepResultProxy
|
|
536
|
+
def initialize(result)
|
|
537
|
+
@result = result
|
|
538
|
+
end
|
|
539
|
+
|
|
540
|
+
# Delegate content access
|
|
541
|
+
def content
|
|
542
|
+
@result&.content
|
|
543
|
+
end
|
|
544
|
+
|
|
545
|
+
# Allow hash-like access to content
|
|
546
|
+
def [](key)
|
|
547
|
+
content&.[](key)
|
|
548
|
+
end
|
|
549
|
+
|
|
550
|
+
# Allow method access to content hash keys
|
|
551
|
+
def method_missing(name, *args, &block)
|
|
552
|
+
if @result.respond_to?(name)
|
|
553
|
+
@result.send(name, *args, &block)
|
|
554
|
+
elsif content.is_a?(Hash) && content.key?(name)
|
|
555
|
+
content[name]
|
|
556
|
+
elsif content.is_a?(Hash) && content.key?(name.to_s)
|
|
557
|
+
content[name.to_s]
|
|
558
|
+
else
|
|
559
|
+
super
|
|
560
|
+
end
|
|
561
|
+
end
|
|
562
|
+
|
|
563
|
+
def respond_to_missing?(name, include_private = false)
|
|
564
|
+
@result.respond_to?(name) ||
|
|
565
|
+
(content.is_a?(Hash) && (content.key?(name) || content.key?(name.to_s))) ||
|
|
566
|
+
super
|
|
567
|
+
end
|
|
568
|
+
|
|
569
|
+
def to_h
|
|
570
|
+
content.is_a?(Hash) ? content : { value: content }
|
|
571
|
+
end
|
|
572
|
+
end
|
|
573
|
+
end
|
|
574
|
+
end
|
|
575
|
+
end
|
|
576
|
+
end
|
|
@@ -232,14 +232,9 @@ module RubyLLM
|
|
|
232
232
|
|
|
233
233
|
# Returns the workflow type name for storage
|
|
234
234
|
#
|
|
235
|
-
# @return [String] The workflow type
|
|
235
|
+
# @return [String] The workflow type
|
|
236
236
|
def workflow_type_name
|
|
237
|
-
|
|
238
|
-
when Workflow::Pipeline then "pipeline"
|
|
239
|
-
when Workflow::Parallel then "parallel"
|
|
240
|
-
when Workflow::Router then "router"
|
|
241
|
-
else "workflow"
|
|
242
|
-
end
|
|
237
|
+
"workflow"
|
|
243
238
|
end
|
|
244
239
|
|
|
245
240
|
# Hook for subclasses to add custom metadata
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
module Agents
|
|
5
|
+
class Workflow
|
|
6
|
+
module Notifiers
|
|
7
|
+
# Base class for approval notification adapters
|
|
8
|
+
#
|
|
9
|
+
# Subclasses should implement the #notify and optionally #remind methods
|
|
10
|
+
# to send notifications through their respective channels.
|
|
11
|
+
#
|
|
12
|
+
# @example Creating a custom notifier
|
|
13
|
+
# class SmsNotifier < Base
|
|
14
|
+
# def notify(approval, message)
|
|
15
|
+
# # Send SMS via Twilio
|
|
16
|
+
# end
|
|
17
|
+
# end
|
|
18
|
+
#
|
|
19
|
+
# @api public
|
|
20
|
+
class Base
|
|
21
|
+
# Send a notification for an approval request
|
|
22
|
+
#
|
|
23
|
+
# @param approval [Approval] The approval request
|
|
24
|
+
# @param message [String] The notification message
|
|
25
|
+
# @return [Boolean] true if notification was sent successfully
|
|
26
|
+
def notify(approval, message)
|
|
27
|
+
raise NotImplementedError, "#{self.class}#notify must be implemented"
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Send a reminder for a pending approval
|
|
31
|
+
#
|
|
32
|
+
# @param approval [Approval] The approval request
|
|
33
|
+
# @param message [String] The reminder message
|
|
34
|
+
# @return [Boolean] true if reminder was sent successfully
|
|
35
|
+
def remind(approval, message)
|
|
36
|
+
notify(approval, "[Reminder] #{message}")
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Send an escalation notice
|
|
40
|
+
#
|
|
41
|
+
# @param approval [Approval] The approval request
|
|
42
|
+
# @param message [String] The escalation message
|
|
43
|
+
# @param escalate_to [String] The escalation target
|
|
44
|
+
# @return [Boolean] true if escalation was sent successfully
|
|
45
|
+
def escalate(approval, message, escalate_to:)
|
|
46
|
+
notify(approval, "[Escalation to #{escalate_to}] #{message}")
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Registry for notifier instances
|
|
51
|
+
#
|
|
52
|
+
# @api private
|
|
53
|
+
class Registry
|
|
54
|
+
class << self
|
|
55
|
+
# Returns the registered notifiers
|
|
56
|
+
#
|
|
57
|
+
# @return [Hash<Symbol, Base>]
|
|
58
|
+
def notifiers
|
|
59
|
+
@notifiers ||= {}
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Register a notifier
|
|
63
|
+
#
|
|
64
|
+
# @param name [Symbol] The notifier name
|
|
65
|
+
# @param notifier [Base] The notifier instance
|
|
66
|
+
# @return [void]
|
|
67
|
+
def register(name, notifier)
|
|
68
|
+
notifiers[name.to_sym] = notifier
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Get a registered notifier
|
|
72
|
+
#
|
|
73
|
+
# @param name [Symbol] The notifier name
|
|
74
|
+
# @return [Base, nil]
|
|
75
|
+
def get(name)
|
|
76
|
+
notifiers[name.to_sym]
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Check if a notifier is registered
|
|
80
|
+
#
|
|
81
|
+
# @param name [Symbol] The notifier name
|
|
82
|
+
# @return [Boolean]
|
|
83
|
+
def registered?(name)
|
|
84
|
+
notifiers.key?(name.to_sym)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Send notification through specified channels
|
|
88
|
+
#
|
|
89
|
+
# @param approval [Approval] The approval request
|
|
90
|
+
# @param message [String] The notification message
|
|
91
|
+
# @param channels [Array<Symbol>] The notification channels
|
|
92
|
+
# @return [Hash<Symbol, Boolean>] Results per channel
|
|
93
|
+
def notify_all(approval, message, channels:)
|
|
94
|
+
results = {}
|
|
95
|
+
channels.each do |channel|
|
|
96
|
+
notifier = get(channel)
|
|
97
|
+
if notifier
|
|
98
|
+
results[channel] = notifier.notify(approval, message)
|
|
99
|
+
else
|
|
100
|
+
results[channel] = false
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
results
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Reset the registry (useful for testing)
|
|
107
|
+
#
|
|
108
|
+
# @return [void]
|
|
109
|
+
def reset!
|
|
110
|
+
@notifiers = {}
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|