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.
Files changed (143) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +142 -0
  3. data/LICENSE +190 -0
  4. data/README.md +517 -0
  5. data/examples/agentic_workflows/llm_chat.rb +106 -0
  6. data/examples/dynamic_workflow.rb +177 -0
  7. data/examples/event_handler.rb +94 -0
  8. data/examples/event_listener_examples.rb +430 -0
  9. data/examples/helloworld/greetings_worker.rb +24 -0
  10. data/examples/helloworld/helloworld.rb +99 -0
  11. data/examples/kitchensink.rb +213 -0
  12. data/examples/metadata_journey.rb +189 -0
  13. data/examples/metrics_example.rb +284 -0
  14. data/examples/new_dsl_demo.rb +141 -0
  15. data/examples/orkes/http_poll.rb +83 -0
  16. data/examples/orkes/secrets_example.rb +69 -0
  17. data/examples/orkes/wait_for_webhook.rb +90 -0
  18. data/examples/prompt_journey.rb +245 -0
  19. data/examples/rag_workflow.rb +167 -0
  20. data/examples/schedule_journey.rb +244 -0
  21. data/examples/simple_worker.rb +125 -0
  22. data/examples/simple_workflow.rb +89 -0
  23. data/examples/task_context_example.rb +257 -0
  24. data/examples/task_listener_example.rb +192 -0
  25. data/examples/worker_configuration_example.rb +282 -0
  26. data/examples/workflow_dsl.rb +316 -0
  27. data/examples/workflow_ops.rb +305 -0
  28. data/lib/conductor/client/authorization_client.rb +238 -0
  29. data/lib/conductor/client/integration_client.rb +108 -0
  30. data/lib/conductor/client/metadata_client.rb +139 -0
  31. data/lib/conductor/client/prompt_client.rb +58 -0
  32. data/lib/conductor/client/scheduler_client.rb +132 -0
  33. data/lib/conductor/client/schema_client.rb +32 -0
  34. data/lib/conductor/client/secret_client.rb +48 -0
  35. data/lib/conductor/client/task_client.rb +168 -0
  36. data/lib/conductor/client/workflow_client.rb +242 -0
  37. data/lib/conductor/configuration/authentication_settings.rb +17 -0
  38. data/lib/conductor/configuration.rb +103 -0
  39. data/lib/conductor/exceptions.rb +86 -0
  40. data/lib/conductor/http/api/application_resource_api.rb +107 -0
  41. data/lib/conductor/http/api/authorization_resource_api.rb +56 -0
  42. data/lib/conductor/http/api/event_resource_api.rb +133 -0
  43. data/lib/conductor/http/api/gateway_auth_resource_api.rb +48 -0
  44. data/lib/conductor/http/api/group_resource_api.rb +76 -0
  45. data/lib/conductor/http/api/integration_resource_api.rb +145 -0
  46. data/lib/conductor/http/api/metadata_resource_api.rb +231 -0
  47. data/lib/conductor/http/api/prompt_resource_api.rb +81 -0
  48. data/lib/conductor/http/api/role_resource_api.rb +60 -0
  49. data/lib/conductor/http/api/scheduler_resource_api.rb +211 -0
  50. data/lib/conductor/http/api/schema_resource_api.rb +82 -0
  51. data/lib/conductor/http/api/secret_resource_api.rb +134 -0
  52. data/lib/conductor/http/api/task_resource_api.rb +321 -0
  53. data/lib/conductor/http/api/token_resource_api.rb +42 -0
  54. data/lib/conductor/http/api/user_resource_api.rb +59 -0
  55. data/lib/conductor/http/api/workflow_bulk_resource_api.rb +91 -0
  56. data/lib/conductor/http/api/workflow_resource_api.rb +451 -0
  57. data/lib/conductor/http/api_client.rb +437 -0
  58. data/lib/conductor/http/models/authentication_config.rb +67 -0
  59. data/lib/conductor/http/models/authorization_request.rb +39 -0
  60. data/lib/conductor/http/models/base_model.rb +162 -0
  61. data/lib/conductor/http/models/bulk_response.rb +39 -0
  62. data/lib/conductor/http/models/conductor_application.rb +39 -0
  63. data/lib/conductor/http/models/conductor_user.rb +53 -0
  64. data/lib/conductor/http/models/create_or_update_application_request.rb +24 -0
  65. data/lib/conductor/http/models/create_or_update_role_request.rb +27 -0
  66. data/lib/conductor/http/models/event_handler.rb +130 -0
  67. data/lib/conductor/http/models/generate_token_request.rb +27 -0
  68. data/lib/conductor/http/models/group.rb +36 -0
  69. data/lib/conductor/http/models/integration.rb +70 -0
  70. data/lib/conductor/http/models/integration_api.rb +53 -0
  71. data/lib/conductor/http/models/integration_api_update.rb +43 -0
  72. data/lib/conductor/http/models/integration_update.rb +36 -0
  73. data/lib/conductor/http/models/permission.rb +24 -0
  74. data/lib/conductor/http/models/poll_data.rb +33 -0
  75. data/lib/conductor/http/models/prompt_template.rb +59 -0
  76. data/lib/conductor/http/models/prompt_template_test_request.rb +43 -0
  77. data/lib/conductor/http/models/rerun_workflow_request.rb +37 -0
  78. data/lib/conductor/http/models/role.rb +27 -0
  79. data/lib/conductor/http/models/schema_def.rb +59 -0
  80. data/lib/conductor/http/models/search_result.rb +187 -0
  81. data/lib/conductor/http/models/skip_task_request.rb +27 -0
  82. data/lib/conductor/http/models/start_workflow_request.rb +68 -0
  83. data/lib/conductor/http/models/subject_ref.rb +35 -0
  84. data/lib/conductor/http/models/tag_object.rb +36 -0
  85. data/lib/conductor/http/models/target_ref.rb +39 -0
  86. data/lib/conductor/http/models/task.rb +156 -0
  87. data/lib/conductor/http/models/task_def.rb +95 -0
  88. data/lib/conductor/http/models/task_exec_log.rb +30 -0
  89. data/lib/conductor/http/models/task_result.rb +115 -0
  90. data/lib/conductor/http/models/task_result_status.rb +24 -0
  91. data/lib/conductor/http/models/token.rb +33 -0
  92. data/lib/conductor/http/models/upsert_group_request.rb +30 -0
  93. data/lib/conductor/http/models/upsert_user_request.rb +39 -0
  94. data/lib/conductor/http/models/workflow.rb +202 -0
  95. data/lib/conductor/http/models/workflow_def.rb +73 -0
  96. data/lib/conductor/http/models/workflow_schedule.rb +100 -0
  97. data/lib/conductor/http/models/workflow_state_update.rb +30 -0
  98. data/lib/conductor/http/models/workflow_status_constants.rb +57 -0
  99. data/lib/conductor/http/models/workflow_task.rb +169 -0
  100. data/lib/conductor/http/models/workflow_test_request.rb +67 -0
  101. data/lib/conductor/http/rest_client.rb +211 -0
  102. data/lib/conductor/orkes/models/access_key.rb +56 -0
  103. data/lib/conductor/orkes/models/granted_permission.rb +27 -0
  104. data/lib/conductor/orkes/models/metadata_tag.rb +15 -0
  105. data/lib/conductor/orkes/models/rate_limit_tag.rb +15 -0
  106. data/lib/conductor/orkes/orkes_clients.rb +69 -0
  107. data/lib/conductor/version.rb +5 -0
  108. data/lib/conductor/worker/events/conductor_event.rb +40 -0
  109. data/lib/conductor/worker/events/global_dispatcher.rb +37 -0
  110. data/lib/conductor/worker/events/http_events.rb +25 -0
  111. data/lib/conductor/worker/events/listener_registry.rb +40 -0
  112. data/lib/conductor/worker/events/listeners.rb +34 -0
  113. data/lib/conductor/worker/events/sync_event_dispatcher.rb +78 -0
  114. data/lib/conductor/worker/events/task_runner_events.rb +271 -0
  115. data/lib/conductor/worker/events/workflow_events.rb +49 -0
  116. data/lib/conductor/worker/fiber_executor.rb +532 -0
  117. data/lib/conductor/worker/ractor_task_runner.rb +501 -0
  118. data/lib/conductor/worker/task_context.rb +114 -0
  119. data/lib/conductor/worker/task_definition_registrar.rb +322 -0
  120. data/lib/conductor/worker/task_handler.rb +360 -0
  121. data/lib/conductor/worker/task_in_progress.rb +60 -0
  122. data/lib/conductor/worker/task_runner.rb +538 -0
  123. data/lib/conductor/worker/telemetry/metrics_collector.rb +196 -0
  124. data/lib/conductor/worker/telemetry/prometheus_backend.rb +224 -0
  125. data/lib/conductor/worker/worker.rb +355 -0
  126. data/lib/conductor/worker/worker_config.rb +154 -0
  127. data/lib/conductor/worker/worker_registry.rb +71 -0
  128. data/lib/conductor/workflow/dsl/input_ref.rb +37 -0
  129. data/lib/conductor/workflow/dsl/output_ref.rb +44 -0
  130. data/lib/conductor/workflow/dsl/parallel_builder.rb +49 -0
  131. data/lib/conductor/workflow/dsl/switch_builder.rb +74 -0
  132. data/lib/conductor/workflow/dsl/task_ref.rb +178 -0
  133. data/lib/conductor/workflow/dsl/workflow_builder.rb +1016 -0
  134. data/lib/conductor/workflow/dsl/workflow_definition.rb +150 -0
  135. data/lib/conductor/workflow/llm/chat_message.rb +47 -0
  136. data/lib/conductor/workflow/llm/embedding_model.rb +19 -0
  137. data/lib/conductor/workflow/llm/tool_call.rb +43 -0
  138. data/lib/conductor/workflow/llm/tool_spec.rb +46 -0
  139. data/lib/conductor/workflow/task_type.rb +68 -0
  140. data/lib/conductor/workflow/timeout_policy.rb +31 -0
  141. data/lib/conductor/workflow/workflow_executor.rb +373 -0
  142. data/lib/conductor.rb +192 -0
  143. 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