conductor_ruby 0.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 +7 -0
- data/CHANGELOG.md +142 -0
- data/LICENSE +190 -0
- data/README.md +517 -0
- data/examples/agentic_workflows/llm_chat.rb +106 -0
- data/examples/dynamic_workflow.rb +177 -0
- data/examples/event_handler.rb +94 -0
- data/examples/event_listener_examples.rb +430 -0
- data/examples/helloworld/greetings_worker.rb +24 -0
- data/examples/helloworld/helloworld.rb +99 -0
- data/examples/kitchensink.rb +213 -0
- data/examples/metadata_journey.rb +189 -0
- data/examples/metrics_example.rb +284 -0
- data/examples/new_dsl_demo.rb +141 -0
- data/examples/orkes/http_poll.rb +83 -0
- data/examples/orkes/secrets_example.rb +69 -0
- data/examples/orkes/wait_for_webhook.rb +90 -0
- data/examples/prompt_journey.rb +245 -0
- data/examples/rag_workflow.rb +167 -0
- data/examples/schedule_journey.rb +244 -0
- data/examples/simple_worker.rb +125 -0
- data/examples/simple_workflow.rb +89 -0
- data/examples/task_context_example.rb +257 -0
- data/examples/task_listener_example.rb +192 -0
- data/examples/worker_configuration_example.rb +282 -0
- data/examples/workflow_dsl.rb +316 -0
- data/examples/workflow_ops.rb +305 -0
- data/lib/conductor/client/authorization_client.rb +238 -0
- data/lib/conductor/client/integration_client.rb +108 -0
- data/lib/conductor/client/metadata_client.rb +139 -0
- data/lib/conductor/client/prompt_client.rb +58 -0
- data/lib/conductor/client/scheduler_client.rb +132 -0
- data/lib/conductor/client/schema_client.rb +32 -0
- data/lib/conductor/client/secret_client.rb +48 -0
- data/lib/conductor/client/task_client.rb +168 -0
- data/lib/conductor/client/workflow_client.rb +242 -0
- data/lib/conductor/configuration/authentication_settings.rb +17 -0
- data/lib/conductor/configuration.rb +103 -0
- data/lib/conductor/exceptions.rb +86 -0
- data/lib/conductor/http/api/application_resource_api.rb +107 -0
- data/lib/conductor/http/api/authorization_resource_api.rb +56 -0
- data/lib/conductor/http/api/event_resource_api.rb +133 -0
- data/lib/conductor/http/api/gateway_auth_resource_api.rb +48 -0
- data/lib/conductor/http/api/group_resource_api.rb +76 -0
- data/lib/conductor/http/api/integration_resource_api.rb +145 -0
- data/lib/conductor/http/api/metadata_resource_api.rb +231 -0
- data/lib/conductor/http/api/prompt_resource_api.rb +81 -0
- data/lib/conductor/http/api/role_resource_api.rb +60 -0
- data/lib/conductor/http/api/scheduler_resource_api.rb +211 -0
- data/lib/conductor/http/api/schema_resource_api.rb +82 -0
- data/lib/conductor/http/api/secret_resource_api.rb +134 -0
- data/lib/conductor/http/api/task_resource_api.rb +321 -0
- data/lib/conductor/http/api/token_resource_api.rb +42 -0
- data/lib/conductor/http/api/user_resource_api.rb +59 -0
- data/lib/conductor/http/api/workflow_bulk_resource_api.rb +91 -0
- data/lib/conductor/http/api/workflow_resource_api.rb +451 -0
- data/lib/conductor/http/api_client.rb +437 -0
- data/lib/conductor/http/models/authentication_config.rb +67 -0
- data/lib/conductor/http/models/authorization_request.rb +39 -0
- data/lib/conductor/http/models/base_model.rb +162 -0
- data/lib/conductor/http/models/bulk_response.rb +39 -0
- data/lib/conductor/http/models/conductor_application.rb +39 -0
- data/lib/conductor/http/models/conductor_user.rb +53 -0
- data/lib/conductor/http/models/create_or_update_application_request.rb +24 -0
- data/lib/conductor/http/models/create_or_update_role_request.rb +27 -0
- data/lib/conductor/http/models/event_handler.rb +130 -0
- data/lib/conductor/http/models/generate_token_request.rb +27 -0
- data/lib/conductor/http/models/group.rb +36 -0
- data/lib/conductor/http/models/integration.rb +70 -0
- data/lib/conductor/http/models/integration_api.rb +53 -0
- data/lib/conductor/http/models/integration_api_update.rb +43 -0
- data/lib/conductor/http/models/integration_update.rb +36 -0
- data/lib/conductor/http/models/permission.rb +24 -0
- data/lib/conductor/http/models/poll_data.rb +33 -0
- data/lib/conductor/http/models/prompt_template.rb +59 -0
- data/lib/conductor/http/models/prompt_template_test_request.rb +43 -0
- data/lib/conductor/http/models/rerun_workflow_request.rb +37 -0
- data/lib/conductor/http/models/role.rb +27 -0
- data/lib/conductor/http/models/schema_def.rb +59 -0
- data/lib/conductor/http/models/search_result.rb +187 -0
- data/lib/conductor/http/models/skip_task_request.rb +27 -0
- data/lib/conductor/http/models/start_workflow_request.rb +68 -0
- data/lib/conductor/http/models/subject_ref.rb +35 -0
- data/lib/conductor/http/models/tag_object.rb +36 -0
- data/lib/conductor/http/models/target_ref.rb +39 -0
- data/lib/conductor/http/models/task.rb +156 -0
- data/lib/conductor/http/models/task_def.rb +95 -0
- data/lib/conductor/http/models/task_exec_log.rb +30 -0
- data/lib/conductor/http/models/task_result.rb +115 -0
- data/lib/conductor/http/models/task_result_status.rb +24 -0
- data/lib/conductor/http/models/token.rb +33 -0
- data/lib/conductor/http/models/upsert_group_request.rb +30 -0
- data/lib/conductor/http/models/upsert_user_request.rb +39 -0
- data/lib/conductor/http/models/workflow.rb +202 -0
- data/lib/conductor/http/models/workflow_def.rb +73 -0
- data/lib/conductor/http/models/workflow_schedule.rb +100 -0
- data/lib/conductor/http/models/workflow_state_update.rb +30 -0
- data/lib/conductor/http/models/workflow_status_constants.rb +57 -0
- data/lib/conductor/http/models/workflow_task.rb +169 -0
- data/lib/conductor/http/models/workflow_test_request.rb +67 -0
- data/lib/conductor/http/rest_client.rb +211 -0
- data/lib/conductor/orkes/models/access_key.rb +56 -0
- data/lib/conductor/orkes/models/granted_permission.rb +27 -0
- data/lib/conductor/orkes/models/metadata_tag.rb +15 -0
- data/lib/conductor/orkes/models/rate_limit_tag.rb +15 -0
- data/lib/conductor/orkes/orkes_clients.rb +69 -0
- data/lib/conductor/version.rb +5 -0
- data/lib/conductor/worker/events/conductor_event.rb +40 -0
- data/lib/conductor/worker/events/global_dispatcher.rb +37 -0
- data/lib/conductor/worker/events/http_events.rb +25 -0
- data/lib/conductor/worker/events/listener_registry.rb +40 -0
- data/lib/conductor/worker/events/listeners.rb +34 -0
- data/lib/conductor/worker/events/sync_event_dispatcher.rb +78 -0
- data/lib/conductor/worker/events/task_runner_events.rb +271 -0
- data/lib/conductor/worker/events/workflow_events.rb +49 -0
- data/lib/conductor/worker/fiber_executor.rb +532 -0
- data/lib/conductor/worker/ractor_task_runner.rb +501 -0
- data/lib/conductor/worker/task_context.rb +114 -0
- data/lib/conductor/worker/task_definition_registrar.rb +322 -0
- data/lib/conductor/worker/task_handler.rb +360 -0
- data/lib/conductor/worker/task_in_progress.rb +60 -0
- data/lib/conductor/worker/task_runner.rb +538 -0
- data/lib/conductor/worker/telemetry/metrics_collector.rb +196 -0
- data/lib/conductor/worker/telemetry/prometheus_backend.rb +224 -0
- data/lib/conductor/worker/worker.rb +355 -0
- data/lib/conductor/worker/worker_config.rb +154 -0
- data/lib/conductor/worker/worker_registry.rb +71 -0
- data/lib/conductor/workflow/dsl/input_ref.rb +37 -0
- data/lib/conductor/workflow/dsl/output_ref.rb +44 -0
- data/lib/conductor/workflow/dsl/parallel_builder.rb +49 -0
- data/lib/conductor/workflow/dsl/switch_builder.rb +74 -0
- data/lib/conductor/workflow/dsl/task_ref.rb +178 -0
- data/lib/conductor/workflow/dsl/workflow_builder.rb +1016 -0
- data/lib/conductor/workflow/dsl/workflow_definition.rb +150 -0
- data/lib/conductor/workflow/llm/chat_message.rb +47 -0
- data/lib/conductor/workflow/llm/embedding_model.rb +19 -0
- data/lib/conductor/workflow/llm/tool_call.rb +43 -0
- data/lib/conductor/workflow/llm/tool_spec.rb +46 -0
- data/lib/conductor/workflow/task_type.rb +68 -0
- data/lib/conductor/workflow/timeout_policy.rb +31 -0
- data/lib/conductor/workflow/workflow_executor.rb +373 -0
- data/lib/conductor.rb +192 -0
- metadata +359 -0
|
@@ -0,0 +1,1016 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../task_type'
|
|
4
|
+
|
|
5
|
+
module Conductor
|
|
6
|
+
module Workflow
|
|
7
|
+
module Dsl
|
|
8
|
+
# WorkflowBuilder is the core DSL engine for building Conductor workflows.
|
|
9
|
+
# It provides Ruby-idiomatic methods for defining tasks and control flow.
|
|
10
|
+
#
|
|
11
|
+
# @example Simple workflow
|
|
12
|
+
# builder = WorkflowBuilder.new('my_workflow', version: 1)
|
|
13
|
+
# user = builder.simple :get_user, user_id: builder.wf[:user_id]
|
|
14
|
+
# builder.simple :send_email, email: user[:email]
|
|
15
|
+
#
|
|
16
|
+
class WorkflowBuilder
|
|
17
|
+
attr_reader :name, :tasks
|
|
18
|
+
|
|
19
|
+
def initialize(name, version: nil, description: nil, executor: nil)
|
|
20
|
+
@name = name
|
|
21
|
+
@version = version
|
|
22
|
+
@description = description
|
|
23
|
+
@executor = executor
|
|
24
|
+
@tasks = []
|
|
25
|
+
@output_params = {}
|
|
26
|
+
@input_params = []
|
|
27
|
+
@ref_counter = Hash.new(0)
|
|
28
|
+
@timeout_seconds = 60
|
|
29
|
+
@owner_email = nil
|
|
30
|
+
@restartable = true
|
|
31
|
+
@failure_workflow = nil
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Get the workflow version
|
|
35
|
+
# @return [Integer, nil] Workflow version
|
|
36
|
+
attr_reader :version
|
|
37
|
+
|
|
38
|
+
# Returns the workflow input proxy for accessing workflow inputs
|
|
39
|
+
# @return [InputRef] Proxy for workflow.input, workflow.variables, etc.
|
|
40
|
+
# @example
|
|
41
|
+
# wf[:user_id] # => "${workflow.input.user_id}"
|
|
42
|
+
# wf.var(:counter) # => "${workflow.variables.counter}"
|
|
43
|
+
def wf
|
|
44
|
+
@wf ||= InputRef.new
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Configure workflow metadata
|
|
48
|
+
def set_version(v)
|
|
49
|
+
@version = v
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def description(text = nil)
|
|
53
|
+
return @description if text.nil?
|
|
54
|
+
|
|
55
|
+
@description = text
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def timeout(seconds)
|
|
59
|
+
@timeout_seconds = seconds
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def owner_email(email)
|
|
63
|
+
@owner_email = email
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def restartable(value)
|
|
67
|
+
@restartable = value
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def failure_workflow(name)
|
|
71
|
+
@failure_workflow = name
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Define workflow output parameters
|
|
75
|
+
# @param params [Hash] Output parameter mappings
|
|
76
|
+
# @example
|
|
77
|
+
# output user_email: user[:email], order_id: wf[:order_id]
|
|
78
|
+
def output(**params)
|
|
79
|
+
@output_params.merge!(resolve_hash(params))
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# ===================================================================
|
|
83
|
+
# SIMPLE TASK METHODS
|
|
84
|
+
# ===================================================================
|
|
85
|
+
|
|
86
|
+
# Add a SIMPLE task (worker task)
|
|
87
|
+
# @param task_name [Symbol, String] The task name
|
|
88
|
+
# @param inputs [Hash] Input parameters
|
|
89
|
+
# @return [TaskRef] Reference to the created task
|
|
90
|
+
def simple(task_name, **inputs)
|
|
91
|
+
add_task(task_name, TaskType::SIMPLE, inputs, {})
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Add an HTTP task
|
|
95
|
+
# @param task_name [Symbol, String] The task name
|
|
96
|
+
# @param url [String, OutputRef] The URL to call
|
|
97
|
+
# @param method [Symbol, String] HTTP method (:get, :post, :put, :delete, etc.)
|
|
98
|
+
# @param body [Hash, String, nil] Request body
|
|
99
|
+
# @param headers [Hash, nil] Request headers
|
|
100
|
+
# @param options [Hash] Additional options (optional, start_delay, etc.)
|
|
101
|
+
# @return [TaskRef] Reference to the created task
|
|
102
|
+
def http(task_name, url:, method: :get, body: nil, headers: nil, **options)
|
|
103
|
+
http_request = {
|
|
104
|
+
'uri' => resolve_value(url),
|
|
105
|
+
'method' => method.to_s.upcase
|
|
106
|
+
}
|
|
107
|
+
http_request['body'] = resolve_value(body) if body
|
|
108
|
+
http_request['headers'] = resolve_value(headers) if headers
|
|
109
|
+
|
|
110
|
+
add_task(task_name, TaskType::HTTP, { 'http_request' => http_request }, options)
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# Add a WAIT task
|
|
114
|
+
# @param seconds [Integer, nil] Duration to wait in seconds
|
|
115
|
+
# @param until_time [String, nil] Wait until specific time (ISO8601 format)
|
|
116
|
+
# @param options [Hash] Additional options
|
|
117
|
+
# @return [TaskRef] Reference to the created task
|
|
118
|
+
def wait(seconds = nil, until_time: nil, **options)
|
|
119
|
+
inputs = {}
|
|
120
|
+
inputs['duration'] = "#{seconds} seconds" if seconds
|
|
121
|
+
inputs['until'] = until_time if until_time
|
|
122
|
+
|
|
123
|
+
add_task('wait', TaskType::WAIT, inputs, options)
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# Add a TERMINATE task
|
|
127
|
+
# @param status [Symbol, String] Termination status (:completed, :failed)
|
|
128
|
+
# @param reason [String] Reason for termination
|
|
129
|
+
# @param options [Hash] Additional options
|
|
130
|
+
# @return [TaskRef] Reference to the created task
|
|
131
|
+
def terminate(status, reason, **options)
|
|
132
|
+
inputs = {
|
|
133
|
+
'terminationStatus' => status.to_s.upcase,
|
|
134
|
+
'terminationReason' => reason
|
|
135
|
+
}
|
|
136
|
+
add_task('terminate', TaskType::TERMINATE, inputs, options)
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# Add a SUB_WORKFLOW task
|
|
140
|
+
# @param task_name [Symbol, String] The task name
|
|
141
|
+
# @param workflow [String] Name of the workflow to call
|
|
142
|
+
# @param version [Integer, nil] Version of the workflow
|
|
143
|
+
# @param inputs [Hash] Input parameters for the sub-workflow
|
|
144
|
+
# @return [TaskRef] Reference to the created task
|
|
145
|
+
def sub_workflow(task_name, workflow:, version: nil, **inputs)
|
|
146
|
+
add_task(
|
|
147
|
+
task_name,
|
|
148
|
+
TaskType::SUB_WORKFLOW,
|
|
149
|
+
inputs,
|
|
150
|
+
{
|
|
151
|
+
sub_workflow_name: workflow,
|
|
152
|
+
sub_workflow_version: version
|
|
153
|
+
}
|
|
154
|
+
)
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
# Add a HUMAN task
|
|
158
|
+
# @param task_name [Symbol, String] The task name
|
|
159
|
+
# @param assignee [String, nil] Email or ID of the assignee
|
|
160
|
+
# @param display_name [String, nil] Display name for the task
|
|
161
|
+
# @param inputs [Hash] Input parameters
|
|
162
|
+
# @return [TaskRef] Reference to the created task
|
|
163
|
+
def human(task_name, assignee: nil, display_name: nil, **inputs)
|
|
164
|
+
inputs = inputs.merge('assignee' => assignee) if assignee
|
|
165
|
+
inputs = inputs.merge('displayName' => display_name) if display_name
|
|
166
|
+
add_task(task_name, TaskType::HUMAN, inputs, {})
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
# Add a SET_VARIABLE task
|
|
170
|
+
# @param variables [Hash] Variables to set
|
|
171
|
+
# @param options [Hash] Additional options
|
|
172
|
+
# @return [TaskRef] Reference to the created task
|
|
173
|
+
def set(**variables)
|
|
174
|
+
add_task('set_variable', TaskType::SET_VARIABLE, resolve_hash(variables), {})
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
# Add an INLINE (JavaScript) task
|
|
178
|
+
# @param task_name [Symbol, String] The task name
|
|
179
|
+
# @param script [String] JavaScript code to execute
|
|
180
|
+
# @param bindings [Hash] Variable bindings for the script
|
|
181
|
+
# @return [TaskRef] Reference to the created task
|
|
182
|
+
def javascript(task_name, script:, **bindings)
|
|
183
|
+
inputs = resolve_hash(bindings)
|
|
184
|
+
add_task(task_name, TaskType::INLINE, inputs, { expression: script, evaluator_type: 'javascript' })
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
# Add a JSON_JQ_TRANSFORM task
|
|
188
|
+
# @param task_name [Symbol, String] The task name
|
|
189
|
+
# @param query [String] JQ query expression
|
|
190
|
+
# @param inputs [Hash] Input data to transform
|
|
191
|
+
# @return [TaskRef] Reference to the created task
|
|
192
|
+
def jq(task_name, query:, **inputs)
|
|
193
|
+
add_task(task_name, TaskType::JSON_JQ_TRANSFORM, inputs, { query_expression: query })
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
# Add an EVENT task
|
|
197
|
+
# @param task_name [Symbol, String] The task name
|
|
198
|
+
# @param sink [String] Event sink name
|
|
199
|
+
# @param inputs [Hash] Event payload
|
|
200
|
+
# @return [TaskRef] Reference to the created task
|
|
201
|
+
def event(task_name, sink:, **inputs)
|
|
202
|
+
add_task(task_name, TaskType::EVENT, inputs, { sink: sink })
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
# Add a KAFKA_PUBLISH task
|
|
206
|
+
# @param task_name [Symbol, String] The task name
|
|
207
|
+
# @param topic [String] Kafka topic
|
|
208
|
+
# @param value [Object] Message value
|
|
209
|
+
# @param key [String, nil] Message key
|
|
210
|
+
# @param headers [Hash, nil] Message headers
|
|
211
|
+
# @return [TaskRef] Reference to the created task
|
|
212
|
+
def kafka_publish(task_name, topic:, value:, key: nil, headers: nil)
|
|
213
|
+
inputs = {
|
|
214
|
+
'kafka_request' => {
|
|
215
|
+
'topic' => topic,
|
|
216
|
+
'value' => resolve_value(value)
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
inputs['kafka_request']['key'] = key if key
|
|
220
|
+
inputs['kafka_request']['headers'] = resolve_value(headers) if headers
|
|
221
|
+
|
|
222
|
+
add_task(task_name, TaskType::KAFKA_PUBLISH, inputs, {})
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
# Add a START_WORKFLOW task (fire-and-forget, does not wait for completion)
|
|
226
|
+
# @param task_name [Symbol, String] The task name
|
|
227
|
+
# @param workflow [String] Name of the workflow to start
|
|
228
|
+
# @param version [Integer, nil] Workflow version (optional)
|
|
229
|
+
# @param inputs [Hash] Input parameters for the started workflow
|
|
230
|
+
# @return [TaskRef] Reference to the created task
|
|
231
|
+
# @example
|
|
232
|
+
# start_workflow :trigger_async, workflow: 'async_processing', user_id: wf[:user_id]
|
|
233
|
+
def start_workflow(task_name, workflow:, version: nil, **inputs)
|
|
234
|
+
start_workflow_input = {
|
|
235
|
+
'name' => workflow,
|
|
236
|
+
'input' => resolve_hash(inputs)
|
|
237
|
+
}
|
|
238
|
+
start_workflow_input['version'] = version if version
|
|
239
|
+
|
|
240
|
+
add_task(task_name, TaskType::START_WORKFLOW, { 'startWorkflow' => start_workflow_input }, {})
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
# Add a WAIT_FOR_WEBHOOK task (waits for external webhook callback)
|
|
244
|
+
# @param task_name [Symbol, String] The task name
|
|
245
|
+
# @param matches [Hash] Match conditions for the webhook payload
|
|
246
|
+
# @return [TaskRef] Reference to the created task
|
|
247
|
+
# @example
|
|
248
|
+
# wait_for_webhook :payment_callback, matches: { 'order_id' => wf[:order_id] }
|
|
249
|
+
def wait_for_webhook(task_name, matches: {})
|
|
250
|
+
add_task(task_name, TaskType::WAIT_FOR_WEBHOOK, { 'matches' => resolve_hash(matches) }, {})
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
# Add an HTTP_POLL task (polls HTTP endpoint until condition is met)
|
|
254
|
+
# @param task_name [Symbol, String] The task name
|
|
255
|
+
# @param url [String] The URL to poll
|
|
256
|
+
# @param method [Symbol, String] HTTP method (default: :get)
|
|
257
|
+
# @param body [Hash, String, nil] Request body
|
|
258
|
+
# @param headers [Hash, nil] Request headers
|
|
259
|
+
# @param termination_condition [String] JavaScript condition for when to stop polling
|
|
260
|
+
# @param polling_interval [Integer] Polling interval in seconds (default: 60)
|
|
261
|
+
# @param polling_strategy [String] 'FIXED' or 'LINEAR_BACKOFF' (default: 'FIXED')
|
|
262
|
+
# @return [TaskRef] Reference to the created task
|
|
263
|
+
# @example
|
|
264
|
+
# http_poll :check_status, url: 'https://api.example.com/status',
|
|
265
|
+
# termination_condition: '$.response.body.status == "complete"',
|
|
266
|
+
# polling_interval: 30
|
|
267
|
+
def http_poll(task_name, url:, method: :get, body: nil, headers: nil,
|
|
268
|
+
termination_condition: nil, polling_interval: 60, polling_strategy: 'FIXED')
|
|
269
|
+
http_request = {
|
|
270
|
+
'uri' => resolve_value(url),
|
|
271
|
+
'method' => method.to_s.upcase
|
|
272
|
+
}
|
|
273
|
+
http_request['body'] = resolve_value(body) if body
|
|
274
|
+
http_request['headers'] = resolve_value(headers) if headers
|
|
275
|
+
|
|
276
|
+
inputs = {
|
|
277
|
+
'http_request' => http_request,
|
|
278
|
+
'pollingInterval' => polling_interval,
|
|
279
|
+
'pollingStrategy' => polling_strategy
|
|
280
|
+
}
|
|
281
|
+
inputs['terminationCondition'] = termination_condition if termination_condition
|
|
282
|
+
|
|
283
|
+
add_task(task_name, TaskType::HTTP_POLL, inputs, {})
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
# Add a DYNAMIC task (task name determined at runtime)
|
|
287
|
+
# @param task_name [Symbol, String] The base task name
|
|
288
|
+
# @param dynamic_task_param [String] Expression for dynamic task name
|
|
289
|
+
# @param inputs [Hash] Input parameters
|
|
290
|
+
# @return [TaskRef] Reference to the created task
|
|
291
|
+
# @example
|
|
292
|
+
# dynamic :process, dynamic_task_param: '${decide_task_ref.output.taskName}'
|
|
293
|
+
def dynamic(task_name, dynamic_task_param:, **inputs)
|
|
294
|
+
add_task(task_name, TaskType::DYNAMIC, inputs, { dynamic_task_name_param: dynamic_task_param })
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
# Add a DYNAMIC fork task (parallel tasks determined at runtime)
|
|
298
|
+
# @param task_name [Symbol, String] The task name
|
|
299
|
+
# @param tasks_param [String, OutputRef] Expression for dynamic tasks array
|
|
300
|
+
# @param tasks_input_param [String, OutputRef] Expression for task inputs
|
|
301
|
+
# @return [TaskRef] Reference to the created task
|
|
302
|
+
# @example
|
|
303
|
+
# dynamic_fork :parallel_process,
|
|
304
|
+
# tasks_param: generator[:tasks],
|
|
305
|
+
# tasks_input_param: generator[:inputs]
|
|
306
|
+
def dynamic_fork(task_name, tasks_param:, tasks_input_param:)
|
|
307
|
+
add_task(
|
|
308
|
+
task_name,
|
|
309
|
+
TaskType::FORK_JOIN_DYNAMIC,
|
|
310
|
+
{},
|
|
311
|
+
{
|
|
312
|
+
dynamic_fork_tasks_param: resolve_value(tasks_param),
|
|
313
|
+
dynamic_fork_tasks_input_param: resolve_value(tasks_input_param)
|
|
314
|
+
}
|
|
315
|
+
)
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
# Add a GET_DOCUMENT task (retrieve and parse a document from URL)
|
|
319
|
+
# @param task_name [Symbol, String] The task name
|
|
320
|
+
# @param url [String] URL of the document
|
|
321
|
+
# @param media_type [String] MIME type of the document
|
|
322
|
+
# @return [TaskRef] Reference to the created task
|
|
323
|
+
# @example
|
|
324
|
+
# get_document :fetch_pdf, url: 'https://example.com/doc.pdf', media_type: 'application/pdf'
|
|
325
|
+
def get_document(task_name, url:, media_type:)
|
|
326
|
+
inputs = {
|
|
327
|
+
'url' => resolve_value(url),
|
|
328
|
+
'mediaType' => media_type
|
|
329
|
+
}
|
|
330
|
+
add_task(task_name, TaskType::GET_DOCUMENT, inputs, {})
|
|
331
|
+
end
|
|
332
|
+
|
|
333
|
+
# ===================================================================
|
|
334
|
+
# LLM TASK METHODS
|
|
335
|
+
# ===================================================================
|
|
336
|
+
|
|
337
|
+
# Add an LLM_CHAT_COMPLETE task
|
|
338
|
+
# @param task_name [Symbol, String] The task name
|
|
339
|
+
# @param provider [String] LLM provider (e.g., 'openai', 'azure_openai')
|
|
340
|
+
# @param model [String] Model name
|
|
341
|
+
# @param messages [Array<ChatMessage, Hash>] Chat messages
|
|
342
|
+
# @param temperature [Float, nil] Temperature (0.0-1.0)
|
|
343
|
+
# @param top_p [Float, nil] Top-p sampling parameter
|
|
344
|
+
# @param stop_words [Array<String>, nil] Stop sequences
|
|
345
|
+
# @param max_tokens [Integer, nil] Maximum tokens to generate
|
|
346
|
+
# @param options [Hash] Additional options
|
|
347
|
+
# @return [TaskRef] Reference to the created task
|
|
348
|
+
def llm_chat(task_name, provider:, model:, messages: nil, temperature: nil, top_p: nil,
|
|
349
|
+
stop_words: nil, max_tokens: nil, **options)
|
|
350
|
+
converted_messages = messages&.map do |msg|
|
|
351
|
+
if msg.is_a?(Hash)
|
|
352
|
+
Conductor::Workflow::Llm::ChatMessage.new(**msg)
|
|
353
|
+
else
|
|
354
|
+
msg
|
|
355
|
+
end
|
|
356
|
+
end
|
|
357
|
+
|
|
358
|
+
inputs = {
|
|
359
|
+
'llmProvider' => provider,
|
|
360
|
+
'model' => model
|
|
361
|
+
}
|
|
362
|
+
inputs['messages'] = converted_messages.map(&:to_h) if converted_messages
|
|
363
|
+
inputs['temperature'] = temperature if temperature
|
|
364
|
+
inputs['topP'] = top_p if top_p
|
|
365
|
+
inputs['stopWords'] = stop_words if stop_words
|
|
366
|
+
inputs['maxTokens'] = max_tokens if max_tokens
|
|
367
|
+
|
|
368
|
+
add_task(task_name, TaskType::LLM_CHAT_COMPLETE, inputs, options)
|
|
369
|
+
end
|
|
370
|
+
|
|
371
|
+
# Add an LLM_TEXT_COMPLETE task
|
|
372
|
+
# @param task_name [Symbol, String] The task name
|
|
373
|
+
# @param provider [String] LLM provider
|
|
374
|
+
# @param model [String] Model name
|
|
375
|
+
# @param prompt [String] Text prompt
|
|
376
|
+
# @param temperature [Float, nil] Temperature
|
|
377
|
+
# @param max_tokens [Integer, nil] Maximum tokens
|
|
378
|
+
# @param options [Hash] Additional options
|
|
379
|
+
# @return [TaskRef] Reference to the created task
|
|
380
|
+
def llm_complete(task_name, provider:, model:, prompt:, temperature: nil, max_tokens: nil, **options)
|
|
381
|
+
inputs = {
|
|
382
|
+
'llmProvider' => provider,
|
|
383
|
+
'model' => model,
|
|
384
|
+
'prompt' => resolve_value(prompt)
|
|
385
|
+
}
|
|
386
|
+
inputs['temperature'] = temperature if temperature
|
|
387
|
+
inputs['maxTokens'] = max_tokens if max_tokens
|
|
388
|
+
|
|
389
|
+
add_task(task_name, TaskType::LLM_TEXT_COMPLETE, inputs, options)
|
|
390
|
+
end
|
|
391
|
+
|
|
392
|
+
# Add an LLM_GENERATE_EMBEDDINGS task
|
|
393
|
+
# @param task_name [Symbol, String] The task name
|
|
394
|
+
# @param provider [String] LLM provider
|
|
395
|
+
# @param model [String] Model name
|
|
396
|
+
# @param text [String, Array<String>] Text(s) to embed
|
|
397
|
+
# @param options [Hash] Additional options
|
|
398
|
+
# @return [TaskRef] Reference to the created task
|
|
399
|
+
def llm_embed(task_name, provider:, model:, text:, **options)
|
|
400
|
+
inputs = {
|
|
401
|
+
'llmProvider' => provider,
|
|
402
|
+
'model' => model,
|
|
403
|
+
'text' => resolve_value(text)
|
|
404
|
+
}
|
|
405
|
+
add_task(task_name, TaskType::LLM_GENERATE_EMBEDDINGS, inputs, options)
|
|
406
|
+
end
|
|
407
|
+
|
|
408
|
+
# Add an LLM_INDEX_TEXT task
|
|
409
|
+
# @param task_name [Symbol, String] The task name
|
|
410
|
+
# @param vector_db [String] Vector database provider
|
|
411
|
+
# @param namespace [String] Index namespace
|
|
412
|
+
# @param index [String] Index name
|
|
413
|
+
# @param embeddings [Array, OutputRef] Embeddings to index
|
|
414
|
+
# @param doc_id [String, OutputRef, nil] Document ID
|
|
415
|
+
# @param options [Hash] Additional options
|
|
416
|
+
# @return [TaskRef] Reference to the created task
|
|
417
|
+
def llm_index(task_name, vector_db:, namespace:, index:, embeddings:, doc_id: nil, **options)
|
|
418
|
+
inputs = {
|
|
419
|
+
'vectorDB' => vector_db,
|
|
420
|
+
'namespace' => namespace,
|
|
421
|
+
'index' => index,
|
|
422
|
+
'embeddingModelProvider' => resolve_value(embeddings)
|
|
423
|
+
}
|
|
424
|
+
inputs['docId'] = resolve_value(doc_id) if doc_id
|
|
425
|
+
|
|
426
|
+
add_task(task_name, TaskType::LLM_INDEX_TEXT, inputs, options)
|
|
427
|
+
end
|
|
428
|
+
|
|
429
|
+
# Add an LLM_SEARCH_INDEX task
|
|
430
|
+
# @param task_name [Symbol, String] The task name
|
|
431
|
+
# @param vector_db [String] Vector database provider
|
|
432
|
+
# @param namespace [String] Index namespace
|
|
433
|
+
# @param index [String] Index name
|
|
434
|
+
# @param query_embeddings [Array, OutputRef] Query embeddings
|
|
435
|
+
# @param top_k [Integer] Number of results to return
|
|
436
|
+
# @param options [Hash] Additional options
|
|
437
|
+
# @return [TaskRef] Reference to the created task
|
|
438
|
+
def llm_search(task_name, vector_db:, namespace:, index:, query_embeddings:, top_k: 10, **options)
|
|
439
|
+
inputs = {
|
|
440
|
+
'vectorDB' => vector_db,
|
|
441
|
+
'namespace' => namespace,
|
|
442
|
+
'index' => index,
|
|
443
|
+
'queryEmbeddings' => resolve_value(query_embeddings),
|
|
444
|
+
'k' => top_k
|
|
445
|
+
}
|
|
446
|
+
add_task(task_name, TaskType::LLM_SEARCH_INDEX, inputs, options)
|
|
447
|
+
end
|
|
448
|
+
|
|
449
|
+
# Add a GENERATE_IMAGE task
|
|
450
|
+
# @param task_name [Symbol, String] The task name
|
|
451
|
+
# @param provider [String] Image generation provider
|
|
452
|
+
# @param model [String] Model name
|
|
453
|
+
# @param prompt [String] Image generation prompt
|
|
454
|
+
# @param size [String, nil] Image size (e.g., '1024x1024')
|
|
455
|
+
# @param options [Hash] Additional options
|
|
456
|
+
# @return [TaskRef] Reference to the created task
|
|
457
|
+
def generate_image(task_name, provider:, model:, prompt:, size: nil, **options)
|
|
458
|
+
inputs = {
|
|
459
|
+
'llmProvider' => provider,
|
|
460
|
+
'model' => model,
|
|
461
|
+
'prompt' => resolve_value(prompt)
|
|
462
|
+
}
|
|
463
|
+
inputs['size'] = size if size
|
|
464
|
+
|
|
465
|
+
add_task(task_name, TaskType::GENERATE_IMAGE, inputs, options)
|
|
466
|
+
end
|
|
467
|
+
|
|
468
|
+
# Add a GENERATE_AUDIO task (text-to-speech)
|
|
469
|
+
# @param task_name [Symbol, String] The task name
|
|
470
|
+
# @param provider [String] LLM provider integration name
|
|
471
|
+
# @param model [String] Audio generation model name
|
|
472
|
+
# @param text [String, nil] Text to convert to audio
|
|
473
|
+
# @param voice [String, nil] Voice selection
|
|
474
|
+
# @param speed [Float, nil] Playback speed
|
|
475
|
+
# @param response_format [String, nil] Output audio format
|
|
476
|
+
# @param n [Integer] Number of outputs (default: 1)
|
|
477
|
+
# @param options [Hash] Additional options
|
|
478
|
+
# @return [TaskRef] Reference to the created task
|
|
479
|
+
# @example
|
|
480
|
+
# generate_audio :tts, provider: 'openai', model: 'tts-1', text: 'Hello world', voice: 'alloy'
|
|
481
|
+
def generate_audio(task_name, provider:, model:, text: nil, voice: nil, speed: nil,
|
|
482
|
+
response_format: nil, n: 1, **options)
|
|
483
|
+
inputs = {
|
|
484
|
+
'llmProvider' => provider,
|
|
485
|
+
'model' => model,
|
|
486
|
+
'n' => n
|
|
487
|
+
}
|
|
488
|
+
inputs['text'] = resolve_value(text) if text
|
|
489
|
+
inputs['voice'] = voice if voice
|
|
490
|
+
inputs['speed'] = speed if speed
|
|
491
|
+
inputs['responseFormat'] = response_format if response_format
|
|
492
|
+
|
|
493
|
+
add_task(task_name, TaskType::GENERATE_AUDIO, inputs, options)
|
|
494
|
+
end
|
|
495
|
+
|
|
496
|
+
# Add an LLM_STORE_EMBEDDINGS task (store vectors in a vector database)
|
|
497
|
+
# @param task_name [Symbol, String] The task name
|
|
498
|
+
# @param vector_db [String] Vector DB integration name
|
|
499
|
+
# @param index [String] Index/collection name
|
|
500
|
+
# @param embeddings [Array, OutputRef] Embedding vector(s) to store
|
|
501
|
+
# @param namespace [String, nil] Namespace/partition
|
|
502
|
+
# @param id [String, nil] Document ID
|
|
503
|
+
# @param metadata [Hash, nil] Document metadata
|
|
504
|
+
# @param embedding_model [String, nil] Model used to generate embeddings
|
|
505
|
+
# @param embedding_model_provider [String, nil] Provider used to generate embeddings
|
|
506
|
+
# @param options [Hash] Additional options
|
|
507
|
+
# @return [TaskRef] Reference to the created task
|
|
508
|
+
# @example
|
|
509
|
+
# llm_store_embeddings :store_vectors, vector_db: 'pinecone', index: 'docs',
|
|
510
|
+
# embeddings: embed_task[:embeddings], id: 'doc-123'
|
|
511
|
+
def llm_store_embeddings(task_name, vector_db:, index:, embeddings:, namespace: nil,
|
|
512
|
+
id: nil, metadata: nil, embedding_model: nil,
|
|
513
|
+
embedding_model_provider: nil, **options)
|
|
514
|
+
inputs = {
|
|
515
|
+
'vectorDB' => vector_db,
|
|
516
|
+
'index' => index,
|
|
517
|
+
'embeddings' => resolve_value(embeddings)
|
|
518
|
+
}
|
|
519
|
+
inputs['namespace'] = namespace if namespace
|
|
520
|
+
inputs['id'] = resolve_value(id) if id
|
|
521
|
+
inputs['metadata'] = resolve_hash(metadata) if metadata
|
|
522
|
+
inputs['embeddingModel'] = embedding_model if embedding_model
|
|
523
|
+
inputs['embeddingModelProvider'] = embedding_model_provider if embedding_model_provider
|
|
524
|
+
|
|
525
|
+
add_task(task_name, TaskType::LLM_STORE_EMBEDDINGS, inputs, options)
|
|
526
|
+
end
|
|
527
|
+
|
|
528
|
+
# Add an LLM_SEARCH_EMBEDDINGS task (search vector database by embeddings)
|
|
529
|
+
# @param task_name [Symbol, String] The task name
|
|
530
|
+
# @param vector_db [String] Vector DB integration name
|
|
531
|
+
# @param index [String] Index/collection name
|
|
532
|
+
# @param embeddings [Array, OutputRef] Query embedding vector
|
|
533
|
+
# @param namespace [String, nil] Namespace/partition
|
|
534
|
+
# @param max_results [Integer] Maximum results to return (default: 1)
|
|
535
|
+
# @param embedding_model [String, nil] Embedding model name
|
|
536
|
+
# @param embedding_model_provider [String, nil] Embedding provider name
|
|
537
|
+
# @param options [Hash] Additional options
|
|
538
|
+
# @return [TaskRef] Reference to the created task
|
|
539
|
+
# @example
|
|
540
|
+
# llm_search_embeddings :find_similar, vector_db: 'pinecone', index: 'docs',
|
|
541
|
+
# embeddings: query_embed[:embeddings], max_results: 5
|
|
542
|
+
def llm_search_embeddings(task_name, vector_db:, index:, embeddings:, namespace: nil,
|
|
543
|
+
max_results: 1, embedding_model: nil,
|
|
544
|
+
embedding_model_provider: nil, **options)
|
|
545
|
+
inputs = {
|
|
546
|
+
'vectorDB' => vector_db,
|
|
547
|
+
'index' => index,
|
|
548
|
+
'embeddings' => resolve_value(embeddings),
|
|
549
|
+
'maxResults' => max_results
|
|
550
|
+
}
|
|
551
|
+
inputs['namespace'] = namespace if namespace
|
|
552
|
+
inputs['embeddingModel'] = embedding_model if embedding_model
|
|
553
|
+
inputs['embeddingModelProvider'] = embedding_model_provider if embedding_model_provider
|
|
554
|
+
|
|
555
|
+
add_task(task_name, TaskType::LLM_SEARCH_EMBEDDINGS, inputs, options)
|
|
556
|
+
end
|
|
557
|
+
|
|
558
|
+
# Add an LLM_GET_EMBEDDINGS task (retrieve stored embeddings)
|
|
559
|
+
# @param task_name [Symbol, String] The task name
|
|
560
|
+
# @param vector_db [String] Vector DB integration name
|
|
561
|
+
# @param index [String] Index/collection name
|
|
562
|
+
# @param ids [Array<String>, OutputRef] Document IDs to retrieve
|
|
563
|
+
# @param namespace [String, nil] Namespace/partition
|
|
564
|
+
# @param options [Hash] Additional options
|
|
565
|
+
# @return [TaskRef] Reference to the created task
|
|
566
|
+
def llm_get_embeddings(task_name, vector_db:, index:, ids:, namespace: nil, **options)
|
|
567
|
+
inputs = {
|
|
568
|
+
'vectorDB' => vector_db,
|
|
569
|
+
'index' => index,
|
|
570
|
+
'ids' => resolve_value(ids)
|
|
571
|
+
}
|
|
572
|
+
inputs['namespace'] = namespace if namespace
|
|
573
|
+
|
|
574
|
+
add_task(task_name, TaskType::LLM_GET_EMBEDDINGS, inputs, options)
|
|
575
|
+
end
|
|
576
|
+
|
|
577
|
+
# Add a LIST_MCP_TOOLS task (list available tools from MCP server)
|
|
578
|
+
# @param task_name [Symbol, String] The task name
|
|
579
|
+
# @param mcp_server [String] MCP server integration name
|
|
580
|
+
# @param headers [Hash, nil] Optional HTTP headers
|
|
581
|
+
# @param options [Hash] Additional options
|
|
582
|
+
# @return [TaskRef] Reference to the created task
|
|
583
|
+
# @example
|
|
584
|
+
# list_mcp_tools :get_tools, mcp_server: 'my-mcp-server'
|
|
585
|
+
def list_mcp_tools(task_name, mcp_server:, headers: nil, **options)
|
|
586
|
+
inputs = { 'mcpServer' => mcp_server }
|
|
587
|
+
inputs['headers'] = resolve_hash(headers) if headers
|
|
588
|
+
|
|
589
|
+
add_task(task_name, TaskType::LIST_MCP_TOOLS, inputs, options)
|
|
590
|
+
end
|
|
591
|
+
|
|
592
|
+
# Add a CALL_MCP_TOOL task (invoke a tool on MCP server)
|
|
593
|
+
# @param task_name [Symbol, String] The task name
|
|
594
|
+
# @param mcp_server [String] MCP server integration name
|
|
595
|
+
# @param method [String] Tool method name
|
|
596
|
+
# @param arguments [Hash, nil] Arguments to pass to the tool
|
|
597
|
+
# @param headers [Hash, nil] Optional HTTP headers
|
|
598
|
+
# @param options [Hash] Additional options
|
|
599
|
+
# @return [TaskRef] Reference to the created task
|
|
600
|
+
# @example
|
|
601
|
+
# call_mcp_tool :execute_tool, mcp_server: 'my-mcp-server',
|
|
602
|
+
# method: 'search', arguments: { query: 'test' }
|
|
603
|
+
def call_mcp_tool(task_name, mcp_server:, method:, arguments: nil, headers: nil, **options)
|
|
604
|
+
inputs = {
|
|
605
|
+
'mcpServer' => mcp_server,
|
|
606
|
+
'method' => method,
|
|
607
|
+
'arguments' => resolve_hash(arguments || {})
|
|
608
|
+
}
|
|
609
|
+
inputs['headers'] = resolve_hash(headers) if headers
|
|
610
|
+
|
|
611
|
+
add_task(task_name, TaskType::CALL_MCP_TOOL, inputs, options)
|
|
612
|
+
end
|
|
613
|
+
|
|
614
|
+
# ===================================================================
|
|
615
|
+
# CONTROL FLOW METHODS
|
|
616
|
+
# ===================================================================
|
|
617
|
+
|
|
618
|
+
# Create a parallel execution block (FORK_JOIN)
|
|
619
|
+
# @yield Block containing tasks to execute in parallel
|
|
620
|
+
# @return [TaskRef] Reference to the JOIN task
|
|
621
|
+
# @example
|
|
622
|
+
# parallel do
|
|
623
|
+
# simple :task1
|
|
624
|
+
# simple :task2
|
|
625
|
+
# end
|
|
626
|
+
def parallel(&block)
|
|
627
|
+
builder = ParallelBuilder.new(self)
|
|
628
|
+
builder.instance_eval(&block)
|
|
629
|
+
branches = builder.finalize
|
|
630
|
+
|
|
631
|
+
# Create FORK_JOIN task
|
|
632
|
+
fork_ref = add_fork_join_task(branches)
|
|
633
|
+
|
|
634
|
+
# Create JOIN task
|
|
635
|
+
join_on_refs = branches.map { |branch| branch.last.ref_name }
|
|
636
|
+
add_join_task(join_on_refs)
|
|
637
|
+
end
|
|
638
|
+
|
|
639
|
+
# Create a switch/decision block
|
|
640
|
+
# @param expression [String, OutputRef] The expression to evaluate
|
|
641
|
+
# @yield Block containing on/otherwise clauses
|
|
642
|
+
# @return [TaskRef] Reference to the SWITCH task
|
|
643
|
+
# @example
|
|
644
|
+
# decide user[:country] do
|
|
645
|
+
# on 'US' do
|
|
646
|
+
# simple :us_flow
|
|
647
|
+
# end
|
|
648
|
+
# on 'UK' do
|
|
649
|
+
# simple :uk_flow
|
|
650
|
+
# end
|
|
651
|
+
# otherwise do
|
|
652
|
+
# simple :default_flow
|
|
653
|
+
# end
|
|
654
|
+
# end
|
|
655
|
+
def decide(expression, &block)
|
|
656
|
+
builder = SwitchBuilder.new(resolve_value(expression), self)
|
|
657
|
+
builder.instance_eval(&block)
|
|
658
|
+
|
|
659
|
+
add_switch_task(builder)
|
|
660
|
+
end
|
|
661
|
+
|
|
662
|
+
# Create a loop that executes N times
|
|
663
|
+
# @param count [Integer] Number of iterations
|
|
664
|
+
# @yield Block containing tasks to loop
|
|
665
|
+
# @return [TaskRef] Reference to the DO_WHILE task
|
|
666
|
+
# @example
|
|
667
|
+
# loop_times 3 do
|
|
668
|
+
# simple :process_batch
|
|
669
|
+
# end
|
|
670
|
+
def loop_times(count, &block)
|
|
671
|
+
loop_while("$.loop_counter < #{count}", &block)
|
|
672
|
+
end
|
|
673
|
+
|
|
674
|
+
# Create a loop with a custom condition
|
|
675
|
+
# @param condition [String] JavaScript condition to evaluate
|
|
676
|
+
# @yield Block containing tasks to loop
|
|
677
|
+
# @return [TaskRef] Reference to the DO_WHILE task
|
|
678
|
+
# @example
|
|
679
|
+
# loop_while "$.has_more == true" do
|
|
680
|
+
# simple :fetch_page
|
|
681
|
+
# end
|
|
682
|
+
def loop_while(condition, &block)
|
|
683
|
+
# Collect tasks in the loop body
|
|
684
|
+
loop_tasks = []
|
|
685
|
+
collector = TaskCollector.new(self, loop_tasks)
|
|
686
|
+
collector.instance_eval(&block)
|
|
687
|
+
|
|
688
|
+
add_do_while_task(condition, loop_tasks)
|
|
689
|
+
end
|
|
690
|
+
|
|
691
|
+
# Create a loop that iterates over items in an array
|
|
692
|
+
# @param items [OutputRef, String] Expression or reference to array to iterate over
|
|
693
|
+
# @yield Block containing tasks to execute for each item
|
|
694
|
+
# @return [TaskRef] Reference to the DO_WHILE task
|
|
695
|
+
# @example
|
|
696
|
+
# loop_over user_list[:users] do
|
|
697
|
+
# simple :process_user, user: iteration[:item]
|
|
698
|
+
# end
|
|
699
|
+
def loop_over(items, &block)
|
|
700
|
+
# Set up the loop with array iteration pattern
|
|
701
|
+
loop_tasks = []
|
|
702
|
+
collector = LoopCollector.new(self, loop_tasks)
|
|
703
|
+
collector.instance_eval(&block)
|
|
704
|
+
|
|
705
|
+
# Create a set variable task to track iteration
|
|
706
|
+
items_expr = resolve_value(items)
|
|
707
|
+
condition = '$.iteration_index < $.items.length()'
|
|
708
|
+
|
|
709
|
+
add_do_while_task_with_items(condition, loop_tasks, items_expr)
|
|
710
|
+
end
|
|
711
|
+
|
|
712
|
+
# Define an inline sub-workflow that executes as a SUB_WORKFLOW task
|
|
713
|
+
# @param task_name [Symbol, String] The task name for the sub-workflow
|
|
714
|
+
# @param version [Integer] Version of the inline workflow (default: 1)
|
|
715
|
+
# @yield Block containing the sub-workflow definition
|
|
716
|
+
# @return [TaskRef] Reference to the SUB_WORKFLOW task
|
|
717
|
+
# @example
|
|
718
|
+
# inline_workflow :process_order do
|
|
719
|
+
# validate = simple :validate
|
|
720
|
+
# simple :process, data: validate[:result]
|
|
721
|
+
# end
|
|
722
|
+
def inline_workflow(task_name, version: 1, &block)
|
|
723
|
+
# Create a nested builder for the inline workflow
|
|
724
|
+
inline_builder = WorkflowBuilder.new(
|
|
725
|
+
"#{@name}_#{task_name}_inline",
|
|
726
|
+
version: version
|
|
727
|
+
)
|
|
728
|
+
inline_builder.instance_eval(&block)
|
|
729
|
+
|
|
730
|
+
# Get the workflow def from the inline builder
|
|
731
|
+
inline_def = inline_builder.to_workflow_def
|
|
732
|
+
|
|
733
|
+
add_task(
|
|
734
|
+
task_name,
|
|
735
|
+
TaskType::SUB_WORKFLOW,
|
|
736
|
+
{},
|
|
737
|
+
{ inline_workflow_def: inline_def }
|
|
738
|
+
)
|
|
739
|
+
end
|
|
740
|
+
|
|
741
|
+
# Create a branch that only executes if a condition is true
|
|
742
|
+
# @param condition [String, OutputRef] The condition to evaluate
|
|
743
|
+
# @yield Block containing tasks to execute if condition is true
|
|
744
|
+
# @return [TaskRef] Reference to the SWITCH task
|
|
745
|
+
# @example
|
|
746
|
+
# when_true order[:is_premium] do
|
|
747
|
+
# simple :apply_discount
|
|
748
|
+
# end
|
|
749
|
+
def when_true(condition, &block)
|
|
750
|
+
decide condition do
|
|
751
|
+
on 'true', &block
|
|
752
|
+
end
|
|
753
|
+
end
|
|
754
|
+
|
|
755
|
+
# Create a branch that only executes if a condition is false
|
|
756
|
+
# @param condition [String, OutputRef] The condition to evaluate
|
|
757
|
+
# @yield Block containing tasks to execute if condition is false
|
|
758
|
+
# @return [TaskRef] Reference to the SWITCH task
|
|
759
|
+
def when_false(condition, &block)
|
|
760
|
+
decide condition do
|
|
761
|
+
on 'false', &block
|
|
762
|
+
end
|
|
763
|
+
end
|
|
764
|
+
|
|
765
|
+
# ===================================================================
|
|
766
|
+
# INTERNAL METHODS
|
|
767
|
+
# ===================================================================
|
|
768
|
+
|
|
769
|
+
private
|
|
770
|
+
|
|
771
|
+
# Add a task to the workflow
|
|
772
|
+
# @param task_name [Symbol, String] The task name
|
|
773
|
+
# @param task_type [String] The task type constant
|
|
774
|
+
# @param input_parameters [Hash] Input parameters
|
|
775
|
+
# @param options [Hash] Additional task options
|
|
776
|
+
# @return [TaskRef] Reference to the created task
|
|
777
|
+
def add_task(task_name, task_type, input_parameters, options = {})
|
|
778
|
+
ref_name = generate_ref_name(task_name)
|
|
779
|
+
resolved_inputs = resolve_hash(input_parameters)
|
|
780
|
+
|
|
781
|
+
task_ref = TaskRef.new(
|
|
782
|
+
ref_name: ref_name,
|
|
783
|
+
task_name: task_name.to_s,
|
|
784
|
+
task_type: task_type,
|
|
785
|
+
input_parameters: resolved_inputs,
|
|
786
|
+
options: options
|
|
787
|
+
)
|
|
788
|
+
|
|
789
|
+
@tasks << task_ref
|
|
790
|
+
task_ref
|
|
791
|
+
end
|
|
792
|
+
|
|
793
|
+
# Generate a unique reference name for a task
|
|
794
|
+
# @param task_name [Symbol, String] The task name
|
|
795
|
+
# @return [String] Unique reference name
|
|
796
|
+
def generate_ref_name(task_name)
|
|
797
|
+
base = "#{task_name}_ref"
|
|
798
|
+
@ref_counter[base] += 1
|
|
799
|
+
@ref_counter[base] == 1 ? base : "#{base}_#{@ref_counter[base]}"
|
|
800
|
+
end
|
|
801
|
+
|
|
802
|
+
# Resolve a value (OutputRef, Hash, Array, or literal)
|
|
803
|
+
# @param value [Object] The value to resolve
|
|
804
|
+
# @return [Object] Resolved value
|
|
805
|
+
def resolve_value(value)
|
|
806
|
+
case value
|
|
807
|
+
when OutputRef, InputRef
|
|
808
|
+
value.to_s
|
|
809
|
+
when Hash
|
|
810
|
+
resolve_hash(value)
|
|
811
|
+
when Array
|
|
812
|
+
value.map { |v| resolve_value(v) }
|
|
813
|
+
else
|
|
814
|
+
value
|
|
815
|
+
end
|
|
816
|
+
end
|
|
817
|
+
|
|
818
|
+
# Resolve all values in a hash and stringify keys
|
|
819
|
+
# @param hash [Hash] The hash to resolve
|
|
820
|
+
# @return [Hash] Hash with string keys and resolved values
|
|
821
|
+
def resolve_hash(hash)
|
|
822
|
+
hash.transform_keys(&:to_s).transform_values { |v| resolve_value(v) }
|
|
823
|
+
end
|
|
824
|
+
|
|
825
|
+
# Add a FORK_JOIN task
|
|
826
|
+
# @param branches [Array<Array<TaskRef>>] The task branches
|
|
827
|
+
# @return [TaskRef] Reference to the FORK task
|
|
828
|
+
def add_fork_join_task(branches)
|
|
829
|
+
ref_name = generate_ref_name('fork')
|
|
830
|
+
|
|
831
|
+
task_ref = TaskRef.new(
|
|
832
|
+
ref_name: ref_name,
|
|
833
|
+
task_name: 'fork',
|
|
834
|
+
task_type: TaskType::FORK_JOIN,
|
|
835
|
+
input_parameters: {},
|
|
836
|
+
options: { fork_branches: branches }
|
|
837
|
+
)
|
|
838
|
+
|
|
839
|
+
@tasks << task_ref
|
|
840
|
+
task_ref
|
|
841
|
+
end
|
|
842
|
+
|
|
843
|
+
# Add a JOIN task
|
|
844
|
+
# @param join_on [Array<String>] Task ref names to join on
|
|
845
|
+
# @return [TaskRef] Reference to the JOIN task
|
|
846
|
+
def add_join_task(join_on)
|
|
847
|
+
ref_name = generate_ref_name('join')
|
|
848
|
+
|
|
849
|
+
task_ref = TaskRef.new(
|
|
850
|
+
ref_name: ref_name,
|
|
851
|
+
task_name: 'join',
|
|
852
|
+
task_type: TaskType::JOIN,
|
|
853
|
+
input_parameters: {},
|
|
854
|
+
options: { join_on: join_on }
|
|
855
|
+
)
|
|
856
|
+
|
|
857
|
+
@tasks << task_ref
|
|
858
|
+
task_ref
|
|
859
|
+
end
|
|
860
|
+
|
|
861
|
+
# Add a SWITCH task
|
|
862
|
+
# @param builder [SwitchBuilder] The switch builder
|
|
863
|
+
# @return [TaskRef] Reference to the SWITCH task
|
|
864
|
+
def add_switch_task(builder)
|
|
865
|
+
ref_name = generate_ref_name('switch')
|
|
866
|
+
|
|
867
|
+
task_ref = TaskRef.new(
|
|
868
|
+
ref_name: ref_name,
|
|
869
|
+
task_name: 'switch',
|
|
870
|
+
task_type: TaskType::SWITCH,
|
|
871
|
+
input_parameters: {},
|
|
872
|
+
options: {
|
|
873
|
+
expression: builder.expression,
|
|
874
|
+
decision_cases: builder.cases,
|
|
875
|
+
default_case: builder.default
|
|
876
|
+
}
|
|
877
|
+
)
|
|
878
|
+
|
|
879
|
+
@tasks << task_ref
|
|
880
|
+
task_ref
|
|
881
|
+
end
|
|
882
|
+
|
|
883
|
+
# Add a DO_WHILE task
|
|
884
|
+
# @param condition [String] Loop condition
|
|
885
|
+
# @param loop_tasks [Array<TaskRef>] Tasks in the loop body
|
|
886
|
+
# @return [TaskRef] Reference to the DO_WHILE task
|
|
887
|
+
def add_do_while_task(condition, loop_tasks)
|
|
888
|
+
ref_name = generate_ref_name('do_while')
|
|
889
|
+
|
|
890
|
+
task_ref = TaskRef.new(
|
|
891
|
+
ref_name: ref_name,
|
|
892
|
+
task_name: 'do_while',
|
|
893
|
+
task_type: TaskType::DO_WHILE,
|
|
894
|
+
input_parameters: {},
|
|
895
|
+
options: {
|
|
896
|
+
loop_condition: condition,
|
|
897
|
+
loop_over: loop_tasks
|
|
898
|
+
}
|
|
899
|
+
)
|
|
900
|
+
|
|
901
|
+
@tasks << task_ref
|
|
902
|
+
task_ref
|
|
903
|
+
end
|
|
904
|
+
|
|
905
|
+
# Add a DO_WHILE task with items for iteration
|
|
906
|
+
# @param condition [String] Loop condition
|
|
907
|
+
# @param loop_tasks [Array<TaskRef>] Tasks in the loop body
|
|
908
|
+
# @param items_expr [String] Expression for items array
|
|
909
|
+
# @return [TaskRef] Reference to the DO_WHILE task
|
|
910
|
+
def add_do_while_task_with_items(condition, loop_tasks, items_expr)
|
|
911
|
+
ref_name = generate_ref_name('do_while')
|
|
912
|
+
|
|
913
|
+
task_ref = TaskRef.new(
|
|
914
|
+
ref_name: ref_name,
|
|
915
|
+
task_name: 'do_while',
|
|
916
|
+
task_type: TaskType::DO_WHILE,
|
|
917
|
+
input_parameters: {
|
|
918
|
+
'items' => items_expr,
|
|
919
|
+
'iteration_index' => 0
|
|
920
|
+
},
|
|
921
|
+
options: {
|
|
922
|
+
loop_condition: condition,
|
|
923
|
+
loop_over: loop_tasks
|
|
924
|
+
}
|
|
925
|
+
)
|
|
926
|
+
|
|
927
|
+
@tasks << task_ref
|
|
928
|
+
task_ref
|
|
929
|
+
end
|
|
930
|
+
|
|
931
|
+
# Convert the builder to a WorkflowDef model
|
|
932
|
+
# @return [Conductor::Http::Models::WorkflowDef] The workflow definition
|
|
933
|
+
def to_workflow_def
|
|
934
|
+
workflow_tasks = convert_tasks_to_workflow_tasks
|
|
935
|
+
|
|
936
|
+
Conductor::Http::Models::WorkflowDef.new(
|
|
937
|
+
name: @name,
|
|
938
|
+
version: @version,
|
|
939
|
+
description: @description,
|
|
940
|
+
tasks: workflow_tasks,
|
|
941
|
+
input_parameters: @input_params,
|
|
942
|
+
output_parameters: @output_params,
|
|
943
|
+
timeout_seconds: @timeout_seconds,
|
|
944
|
+
owner_email: @owner_email,
|
|
945
|
+
restartable: @restartable,
|
|
946
|
+
failure_workflow: @failure_workflow,
|
|
947
|
+
schema_version: 2
|
|
948
|
+
)
|
|
949
|
+
end
|
|
950
|
+
|
|
951
|
+
# Convert TaskRefs to WorkflowTask models
|
|
952
|
+
# @return [Array<Conductor::Http::Models::WorkflowTask>] Workflow tasks
|
|
953
|
+
def convert_tasks_to_workflow_tasks
|
|
954
|
+
@tasks.flat_map do |task_ref|
|
|
955
|
+
wf_task = task_ref.to_workflow_task
|
|
956
|
+
# FORK_JOIN returns an array [fork_task, join_task]
|
|
957
|
+
wf_task.is_a?(Array) ? wf_task : [wf_task]
|
|
958
|
+
end
|
|
959
|
+
end
|
|
960
|
+
|
|
961
|
+
# Make to_workflow_def accessible to WorkflowDefinition
|
|
962
|
+
public :to_workflow_def
|
|
963
|
+
end
|
|
964
|
+
|
|
965
|
+
# Helper class for collecting tasks in a block
|
|
966
|
+
class TaskCollector
|
|
967
|
+
def initialize(parent_builder, task_array)
|
|
968
|
+
@parent = parent_builder
|
|
969
|
+
@tasks = task_array
|
|
970
|
+
end
|
|
971
|
+
|
|
972
|
+
# Access workflow inputs
|
|
973
|
+
def wf
|
|
974
|
+
@parent.wf
|
|
975
|
+
end
|
|
976
|
+
|
|
977
|
+
def method_missing(name, *args, **kwargs, &block)
|
|
978
|
+
if @parent.respond_to?(name, true)
|
|
979
|
+
task_ref = @parent.send(name, *args, **kwargs, &block)
|
|
980
|
+
@tasks << task_ref if task_ref.is_a?(TaskRef)
|
|
981
|
+
task_ref
|
|
982
|
+
else
|
|
983
|
+
super
|
|
984
|
+
end
|
|
985
|
+
end
|
|
986
|
+
|
|
987
|
+
def respond_to_missing?(name, include_private = false)
|
|
988
|
+
@parent.respond_to?(name, include_private) || super
|
|
989
|
+
end
|
|
990
|
+
end
|
|
991
|
+
|
|
992
|
+
# Helper class for collecting tasks in a loop with iteration access
|
|
993
|
+
class LoopCollector < TaskCollector
|
|
994
|
+
# Access current iteration data within a loop_over block
|
|
995
|
+
# @return [OutputRef] Reference to iteration data
|
|
996
|
+
# @example
|
|
997
|
+
# loop_over items do
|
|
998
|
+
# simple :process, item: iteration[:item], index: iteration[:index]
|
|
999
|
+
# end
|
|
1000
|
+
def iteration
|
|
1001
|
+
@iteration ||= OutputRef.new('do_while_ref.output')
|
|
1002
|
+
end
|
|
1003
|
+
|
|
1004
|
+
# Alias for iteration[:item]
|
|
1005
|
+
def current_item
|
|
1006
|
+
iteration[:item]
|
|
1007
|
+
end
|
|
1008
|
+
|
|
1009
|
+
# Alias for iteration[:index]
|
|
1010
|
+
def current_index
|
|
1011
|
+
iteration[:index]
|
|
1012
|
+
end
|
|
1013
|
+
end
|
|
1014
|
+
end
|
|
1015
|
+
end
|
|
1016
|
+
end
|