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
data/README.md
ADDED
|
@@ -0,0 +1,517 @@
|
|
|
1
|
+
# Conductor Ruby SDK
|
|
2
|
+
|
|
3
|
+
Official Ruby SDK for [Conductor OSS](https://github.com/conductor-oss/conductor) - a durable workflow orchestration engine.
|
|
4
|
+
|
|
5
|
+
[](https://badge.fury.io/rb/conductor_ruby)
|
|
6
|
+
[](LICENSE)
|
|
7
|
+
|
|
8
|
+
## Features
|
|
9
|
+
|
|
10
|
+
- **Full Feature Parity** with Python SDK
|
|
11
|
+
- **Ruby-Idiomatic Workflow DSL** - Clean block-based syntax with 25+ task types
|
|
12
|
+
- **Worker Framework** - Multi-threaded task execution with class-based and block-based workers
|
|
13
|
+
- **LLM/AI Tasks** - Chat completion, embeddings, RAG, image/audio generation
|
|
14
|
+
- **Orkes Cloud Support** - Authentication, secrets, integrations, prompts
|
|
15
|
+
- **Comprehensive Testing** - 400+ unit tests, 110 integration tests
|
|
16
|
+
|
|
17
|
+
## Installation
|
|
18
|
+
|
|
19
|
+
Add to your Gemfile:
|
|
20
|
+
|
|
21
|
+
```ruby
|
|
22
|
+
gem 'conductor_ruby'
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Or install directly:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
gem install conductor_ruby
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Quick Start
|
|
32
|
+
|
|
33
|
+
### Hello World
|
|
34
|
+
|
|
35
|
+
```ruby
|
|
36
|
+
require 'conductor'
|
|
37
|
+
|
|
38
|
+
# Configuration (reads CONDUCTOR_SERVER_URL from environment)
|
|
39
|
+
config = Conductor::Configuration.new
|
|
40
|
+
|
|
41
|
+
# Create clients
|
|
42
|
+
clients = Conductor::Orkes::OrkesClients.new(config)
|
|
43
|
+
executor = clients.get_workflow_executor
|
|
44
|
+
|
|
45
|
+
# Define a worker
|
|
46
|
+
class GreetWorker
|
|
47
|
+
include Conductor::Worker::WorkerModule
|
|
48
|
+
worker_task 'greet'
|
|
49
|
+
|
|
50
|
+
def execute(task)
|
|
51
|
+
name = get_input(task, 'name', 'World')
|
|
52
|
+
{ 'result' => "Hello, #{name}!" }
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Build workflow using new DSL
|
|
57
|
+
workflow = Conductor.workflow :greetings, version: 1, executor: executor do
|
|
58
|
+
greet = simple :greet, name: wf[:name]
|
|
59
|
+
output result: greet[:result]
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Register and execute
|
|
63
|
+
workflow.register(overwrite: true)
|
|
64
|
+
|
|
65
|
+
# Start workers
|
|
66
|
+
runner = Conductor::Worker::TaskRunner.new(config)
|
|
67
|
+
runner.register_worker(GreetWorker.new)
|
|
68
|
+
runner.start
|
|
69
|
+
|
|
70
|
+
# Execute workflow
|
|
71
|
+
result = workflow.execute(input: { 'name' => 'Ruby' }, wait_for_seconds: 30)
|
|
72
|
+
puts "Result: #{result.output['result']}" # => "Hello, Ruby!"
|
|
73
|
+
|
|
74
|
+
runner.stop
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## Workflow DSL
|
|
78
|
+
|
|
79
|
+
The SDK provides a clean, Ruby-idiomatic DSL for building workflows:
|
|
80
|
+
|
|
81
|
+
```ruby
|
|
82
|
+
workflow = Conductor.workflow :order_processing, version: 1, executor: executor do
|
|
83
|
+
# Access workflow inputs with wf[:param]
|
|
84
|
+
user = simple :get_user, user_id: wf[:user_id]
|
|
85
|
+
|
|
86
|
+
# Reference task outputs with task[:field]
|
|
87
|
+
order = simple :validate_order, email: user[:email]
|
|
88
|
+
|
|
89
|
+
# HTTP calls
|
|
90
|
+
http :call_api, url: 'https://api.example.com', method: :post, body: { id: order[:id] }
|
|
91
|
+
|
|
92
|
+
# Parallel execution
|
|
93
|
+
parallel do
|
|
94
|
+
simple :ship_order, order_id: order[:id]
|
|
95
|
+
simple :send_confirmation, email: user[:email]
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Conditional branching
|
|
99
|
+
decide order[:region] do
|
|
100
|
+
on 'US' do
|
|
101
|
+
simple :us_shipping
|
|
102
|
+
end
|
|
103
|
+
on 'EU' do
|
|
104
|
+
simple :eu_shipping
|
|
105
|
+
end
|
|
106
|
+
otherwise do
|
|
107
|
+
terminate :failed, 'Unsupported region'
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# Set workflow output
|
|
112
|
+
output tracking: order[:tracking_number], status: 'completed'
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# Register and execute
|
|
116
|
+
workflow.register(overwrite: true)
|
|
117
|
+
result = workflow.execute(input: { user_id: 123 }, wait_for_seconds: 60)
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
### Task Methods Reference
|
|
121
|
+
|
|
122
|
+
#### Basic Tasks
|
|
123
|
+
|
|
124
|
+
```ruby
|
|
125
|
+
# Simple task (worker execution)
|
|
126
|
+
result = simple :task_name, input1: 'value', input2: wf[:param]
|
|
127
|
+
|
|
128
|
+
# Inline code execution
|
|
129
|
+
jq :transform, query: '.items | map(.name)', input: previous[:data]
|
|
130
|
+
javascript :compute, script: 'return inputs.a + inputs.b', a: 1, b: 2
|
|
131
|
+
|
|
132
|
+
# Set workflow variables
|
|
133
|
+
set_variable :save_state, user_id: user[:id], status: 'active'
|
|
134
|
+
|
|
135
|
+
# Human/manual task
|
|
136
|
+
human :approval, display_name: 'Manager Approval', form_template: 'approval_form'
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
#### HTTP Tasks
|
|
140
|
+
|
|
141
|
+
```ruby
|
|
142
|
+
# HTTP request
|
|
143
|
+
http :call_api,
|
|
144
|
+
url: 'https://api.example.com/users',
|
|
145
|
+
method: :post,
|
|
146
|
+
headers: { 'Authorization' => 'Bearer ${workflow.secrets.api_token}' },
|
|
147
|
+
body: { name: wf[:name], email: wf[:email] }
|
|
148
|
+
|
|
149
|
+
# HTTP polling (wait for condition)
|
|
150
|
+
http_poll :wait_for_ready,
|
|
151
|
+
url: 'https://api.example.com/status/${workflow.input.job_id}',
|
|
152
|
+
method: :get,
|
|
153
|
+
termination_condition: '$.status == "ready"',
|
|
154
|
+
polling_interval: 5,
|
|
155
|
+
polling_strategy: :fixed
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
#### Control Flow
|
|
159
|
+
|
|
160
|
+
```ruby
|
|
161
|
+
# Parallel execution (fork/join)
|
|
162
|
+
parallel do
|
|
163
|
+
simple :branch_a
|
|
164
|
+
simple :branch_b
|
|
165
|
+
simple :branch_c
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
# Conditional branching
|
|
169
|
+
decide order[:status] do
|
|
170
|
+
on 'pending' do
|
|
171
|
+
simple :process_pending
|
|
172
|
+
end
|
|
173
|
+
on 'approved' do
|
|
174
|
+
simple :process_approved
|
|
175
|
+
end
|
|
176
|
+
otherwise do
|
|
177
|
+
simple :handle_unknown
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
# Conditional shortcuts
|
|
182
|
+
when_true user[:is_premium] do
|
|
183
|
+
simple :apply_discount
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
when_false order[:validated] do
|
|
187
|
+
terminate :failed, 'Order validation failed'
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
# Loop over items
|
|
191
|
+
loop_over users[:list], as: :user do
|
|
192
|
+
simple :process_user, user_id: iteration[:user][:id]
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
# Do-while loop
|
|
196
|
+
do_while :retry_loop, condition: '${retry_ref.output.success} == false' do
|
|
197
|
+
simple :retry_operation
|
|
198
|
+
end
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
#### Sub-workflows
|
|
202
|
+
|
|
203
|
+
```ruby
|
|
204
|
+
# Call another workflow
|
|
205
|
+
sub_workflow :process_order,
|
|
206
|
+
workflow_name: 'order_processor',
|
|
207
|
+
version: 2,
|
|
208
|
+
input: { order_id: wf[:order_id] }
|
|
209
|
+
|
|
210
|
+
# Start workflow (fire-and-forget)
|
|
211
|
+
start_workflow :trigger_notification,
|
|
212
|
+
workflow_name: 'send_notifications',
|
|
213
|
+
input: { user_id: user[:id] }
|
|
214
|
+
|
|
215
|
+
# Inline sub-workflow definition
|
|
216
|
+
inline_workflow :nested_process do
|
|
217
|
+
simple :step1
|
|
218
|
+
simple :step2
|
|
219
|
+
end
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
#### Wait and Events
|
|
223
|
+
|
|
224
|
+
```ruby
|
|
225
|
+
# Wait for duration
|
|
226
|
+
wait :pause, duration: '30s' # or '5m', '1h', '2d'
|
|
227
|
+
|
|
228
|
+
# Wait until specific time
|
|
229
|
+
wait :scheduled, until: '2024-12-25T00:00:00Z'
|
|
230
|
+
|
|
231
|
+
# Wait for external webhook
|
|
232
|
+
wait_for_webhook :external_callback,
|
|
233
|
+
matches: { 'type' => 'payment', 'order_id' => '${workflow.input.order_id}' }
|
|
234
|
+
|
|
235
|
+
# Publish event
|
|
236
|
+
event :notify, sink: 'conductor:workflow_events', payload: { status: 'completed' }
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
#### Termination
|
|
240
|
+
|
|
241
|
+
```ruby
|
|
242
|
+
# Complete workflow
|
|
243
|
+
terminate :success, 'Processing completed successfully'
|
|
244
|
+
|
|
245
|
+
# Fail workflow
|
|
246
|
+
terminate :failed, 'Validation error: missing required field'
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
#### Dynamic Tasks
|
|
250
|
+
|
|
251
|
+
```ruby
|
|
252
|
+
# Dynamic task name (resolved at runtime)
|
|
253
|
+
dynamic :run_handler, task_to_execute: wf[:handler_name]
|
|
254
|
+
|
|
255
|
+
# Dynamic fork (parallel tasks determined at runtime)
|
|
256
|
+
dynamic_fork :process_all,
|
|
257
|
+
tasks_input: wf[:items],
|
|
258
|
+
task_name: 'process_item'
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
### LLM/AI Tasks
|
|
262
|
+
|
|
263
|
+
```ruby
|
|
264
|
+
workflow = Conductor.workflow :ai_assistant, executor: executor do
|
|
265
|
+
# Chat completion (messages auto-converted from simple format)
|
|
266
|
+
response = llm_chat :chat,
|
|
267
|
+
provider: 'openai',
|
|
268
|
+
model: 'gpt-4',
|
|
269
|
+
messages: [
|
|
270
|
+
{ role: :system, message: 'You are a helpful assistant.' },
|
|
271
|
+
{ role: :user, message: wf[:question] }
|
|
272
|
+
],
|
|
273
|
+
temperature: 0.7
|
|
274
|
+
|
|
275
|
+
# Text completion
|
|
276
|
+
llm_text :complete,
|
|
277
|
+
provider: 'anthropic',
|
|
278
|
+
model: 'claude-3-sonnet',
|
|
279
|
+
prompt: 'Summarize: ${workflow.input.text}'
|
|
280
|
+
|
|
281
|
+
# Generate embeddings
|
|
282
|
+
embeddings = llm_embeddings :embed,
|
|
283
|
+
provider: 'openai',
|
|
284
|
+
model: 'text-embedding-3-small',
|
|
285
|
+
text: wf[:document]
|
|
286
|
+
|
|
287
|
+
# Store embeddings in vector DB
|
|
288
|
+
llm_store_embeddings :store,
|
|
289
|
+
provider: 'pinecone',
|
|
290
|
+
index: 'documents',
|
|
291
|
+
embeddings: embeddings[:embeddings],
|
|
292
|
+
metadata: { doc_id: wf[:doc_id] }
|
|
293
|
+
|
|
294
|
+
# Search embeddings
|
|
295
|
+
llm_search_embeddings :search,
|
|
296
|
+
provider: 'pinecone',
|
|
297
|
+
index: 'documents',
|
|
298
|
+
query: wf[:search_query],
|
|
299
|
+
max_results: 10
|
|
300
|
+
|
|
301
|
+
# Generate image
|
|
302
|
+
generate_image :create_image,
|
|
303
|
+
provider: 'openai',
|
|
304
|
+
model: 'dall-e-3',
|
|
305
|
+
prompt: 'A sunset over mountains',
|
|
306
|
+
size: '1024x1024'
|
|
307
|
+
|
|
308
|
+
# Generate audio (text-to-speech)
|
|
309
|
+
generate_audio :speak,
|
|
310
|
+
provider: 'openai',
|
|
311
|
+
model: 'tts-1',
|
|
312
|
+
text: response[:content],
|
|
313
|
+
voice: 'nova'
|
|
314
|
+
|
|
315
|
+
# MCP (Model Context Protocol) integration
|
|
316
|
+
tools = list_mcp_tools :get_tools, server_name: 'my_mcp_server'
|
|
317
|
+
|
|
318
|
+
call_mcp_tool :use_tool,
|
|
319
|
+
server_name: 'my_mcp_server',
|
|
320
|
+
tool_name: 'search_documents',
|
|
321
|
+
arguments: { query: wf[:query] }
|
|
322
|
+
|
|
323
|
+
output answer: response[:content]
|
|
324
|
+
end
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
### Output References
|
|
328
|
+
|
|
329
|
+
The DSL uses a clean syntax for referencing outputs:
|
|
330
|
+
|
|
331
|
+
```ruby
|
|
332
|
+
# Workflow input reference
|
|
333
|
+
wf[:user_id] # => '${workflow.input.user_id}'
|
|
334
|
+
|
|
335
|
+
# Task output reference
|
|
336
|
+
task[:field] # => '${task_ref.output.field}'
|
|
337
|
+
task[:nested][:path] # => '${task_ref.output.nested.path}'
|
|
338
|
+
|
|
339
|
+
# Loop iteration references (inside loop_over)
|
|
340
|
+
iteration[:current_item] # Current item being processed
|
|
341
|
+
iteration[:index] # Current index (0-based)
|
|
342
|
+
iteration[:user][:name] # If `as: :user` specified
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
## Examples
|
|
346
|
+
|
|
347
|
+
The `examples/` directory contains comprehensive examples:
|
|
348
|
+
|
|
349
|
+
| Example | Description |
|
|
350
|
+
|---------|-------------|
|
|
351
|
+
| [`helloworld/`](examples/helloworld/) | Simplest complete example - worker + workflow + execution |
|
|
352
|
+
| [`workflow_dsl.rb`](examples/workflow_dsl.rb) | Comprehensive new DSL showcase |
|
|
353
|
+
| [`simple_worker.rb`](examples/simple_worker.rb) | Worker patterns: class-based, block-based, error handling |
|
|
354
|
+
| [`kitchensink.rb`](examples/kitchensink.rb) | All major task types using new DSL |
|
|
355
|
+
| [`dynamic_workflow.rb`](examples/dynamic_workflow.rb) | Create and execute workflows at runtime |
|
|
356
|
+
| [`workflow_ops.rb`](examples/workflow_ops.rb) | Lifecycle operations: pause, resume, restart, retry |
|
|
357
|
+
| [`agentic_workflows/`](examples/agentic_workflows/) | LLM chat and AI workflow examples |
|
|
358
|
+
|
|
359
|
+
Run examples:
|
|
360
|
+
|
|
361
|
+
```bash
|
|
362
|
+
# Set environment variables
|
|
363
|
+
export CONDUCTOR_SERVER_URL=http://localhost:8080/api
|
|
364
|
+
# For Orkes Cloud:
|
|
365
|
+
# export CONDUCTOR_AUTH_KEY=your_key
|
|
366
|
+
# export CONDUCTOR_AUTH_SECRET=your_secret
|
|
367
|
+
|
|
368
|
+
# Run hello world
|
|
369
|
+
cd examples/helloworld && bundle exec ruby helloworld.rb
|
|
370
|
+
|
|
371
|
+
# Run DSL showcase
|
|
372
|
+
bundle exec ruby examples/workflow_dsl.rb
|
|
373
|
+
|
|
374
|
+
# Run kitchen sink
|
|
375
|
+
bundle exec ruby examples/kitchensink.rb
|
|
376
|
+
```
|
|
377
|
+
|
|
378
|
+
## Worker Framework
|
|
379
|
+
|
|
380
|
+
### Class-Based Workers
|
|
381
|
+
|
|
382
|
+
```ruby
|
|
383
|
+
class ImageProcessor
|
|
384
|
+
include Conductor::Worker::WorkerModule
|
|
385
|
+
|
|
386
|
+
worker_task 'process_image', poll_interval: 1, thread_count: 4
|
|
387
|
+
|
|
388
|
+
def execute(task)
|
|
389
|
+
url = get_input(task, 'image_url')
|
|
390
|
+
# Process image...
|
|
391
|
+
|
|
392
|
+
result = Conductor::Http::Models::TaskResult.complete
|
|
393
|
+
result.add_output_data('processed_url', processed_url)
|
|
394
|
+
result.log('Image processed successfully')
|
|
395
|
+
result
|
|
396
|
+
end
|
|
397
|
+
end
|
|
398
|
+
```
|
|
399
|
+
|
|
400
|
+
### Block-Based Workers
|
|
401
|
+
|
|
402
|
+
```ruby
|
|
403
|
+
worker = Conductor::Worker.define('simple_task') do |task|
|
|
404
|
+
input = task.input_data['value']
|
|
405
|
+
{ result: input * 2 } # Return hash for automatic TaskResult
|
|
406
|
+
end
|
|
407
|
+
```
|
|
408
|
+
|
|
409
|
+
### Running Workers
|
|
410
|
+
|
|
411
|
+
```ruby
|
|
412
|
+
runner = Conductor::Worker::TaskRunner.new(config)
|
|
413
|
+
runner.register_worker(ImageProcessor.new)
|
|
414
|
+
runner.register_worker(worker)
|
|
415
|
+
runner.start(threads: 4)
|
|
416
|
+
|
|
417
|
+
# Graceful shutdown
|
|
418
|
+
trap('INT') { runner.stop }
|
|
419
|
+
sleep while runner.running?
|
|
420
|
+
```
|
|
421
|
+
|
|
422
|
+
## Configuration
|
|
423
|
+
|
|
424
|
+
### Environment Variables
|
|
425
|
+
|
|
426
|
+
```bash
|
|
427
|
+
export CONDUCTOR_SERVER_URL=http://localhost:8080/api
|
|
428
|
+
export CONDUCTOR_AUTH_KEY=your_key # For Orkes Cloud
|
|
429
|
+
export CONDUCTOR_AUTH_SECRET=your_secret # For Orkes Cloud
|
|
430
|
+
```
|
|
431
|
+
|
|
432
|
+
### Programmatic
|
|
433
|
+
|
|
434
|
+
```ruby
|
|
435
|
+
config = Conductor::Configuration.new(
|
|
436
|
+
server_api_url: 'https://play.orkes.io/api',
|
|
437
|
+
auth_key: 'your_key',
|
|
438
|
+
auth_secret: 'your_secret',
|
|
439
|
+
auth_token_ttl_min: 45,
|
|
440
|
+
verify_ssl: true
|
|
441
|
+
)
|
|
442
|
+
```
|
|
443
|
+
|
|
444
|
+
## API Coverage
|
|
445
|
+
|
|
446
|
+
### Resource APIs (17 classes)
|
|
447
|
+
|
|
448
|
+
| API | Description |
|
|
449
|
+
|-----|-------------|
|
|
450
|
+
| WorkflowResourceApi | Workflow execution and management |
|
|
451
|
+
| TaskResourceApi | Task polling and updates |
|
|
452
|
+
| MetadataResourceApi | Workflow/task definitions |
|
|
453
|
+
| SchedulerResourceApi | Scheduled workflows |
|
|
454
|
+
| EventResourceApi | Event handlers |
|
|
455
|
+
| WorkflowBulkResourceApi | Bulk operations |
|
|
456
|
+
| PromptResourceApi | AI prompt templates |
|
|
457
|
+
| SecretResourceApi | Secret management |
|
|
458
|
+
| IntegrationResourceApi | External integrations |
|
|
459
|
+
| + 8 more | Authorization, Users, Groups, Roles, etc. |
|
|
460
|
+
|
|
461
|
+
### High-Level Clients (9 classes)
|
|
462
|
+
|
|
463
|
+
```ruby
|
|
464
|
+
clients = Conductor::Orkes::OrkesClients.new(config)
|
|
465
|
+
|
|
466
|
+
workflow_client = clients.get_workflow_client
|
|
467
|
+
task_client = clients.get_task_client
|
|
468
|
+
metadata_client = clients.get_metadata_client
|
|
469
|
+
scheduler_client = clients.get_scheduler_client
|
|
470
|
+
prompt_client = clients.get_prompt_client
|
|
471
|
+
secret_client = clients.get_secret_client
|
|
472
|
+
authorization_client = clients.get_authorization_client
|
|
473
|
+
workflow_executor = clients.get_workflow_executor
|
|
474
|
+
```
|
|
475
|
+
|
|
476
|
+
## Testing
|
|
477
|
+
|
|
478
|
+
```bash
|
|
479
|
+
# Unit tests
|
|
480
|
+
bundle exec rspec spec/conductor/
|
|
481
|
+
|
|
482
|
+
# Integration tests (requires Conductor server)
|
|
483
|
+
CONDUCTOR_SERVER_URL=http://localhost:8080/api bundle exec rspec spec/integration/
|
|
484
|
+
```
|
|
485
|
+
|
|
486
|
+
## Requirements
|
|
487
|
+
|
|
488
|
+
- Ruby 2.6+ (Ruby 3+ recommended)
|
|
489
|
+
- Conductor OSS 3.x or Orkes Cloud
|
|
490
|
+
|
|
491
|
+
## Dependencies
|
|
492
|
+
|
|
493
|
+
- `faraday ~> 2.0` - HTTP client
|
|
494
|
+
- `faraday-net_http_persistent ~> 2.0` - Connection pooling
|
|
495
|
+
- `faraday-retry ~> 2.0` - Automatic retries
|
|
496
|
+
- `concurrent-ruby ~> 1.2` - Thread-safe concurrency
|
|
497
|
+
|
|
498
|
+
## Contributing
|
|
499
|
+
|
|
500
|
+
1. Fork the repository
|
|
501
|
+
2. Create your feature branch (`git checkout -b feature/amazing-feature`)
|
|
502
|
+
3. Run tests (`bundle exec rspec`)
|
|
503
|
+
4. Commit your changes (`git commit -m 'Add amazing feature'`)
|
|
504
|
+
5. Push to the branch (`git push origin feature/amazing-feature`)
|
|
505
|
+
6. Open a Pull Request
|
|
506
|
+
|
|
507
|
+
## License
|
|
508
|
+
|
|
509
|
+
Apache 2.0 - see [LICENSE](LICENSE) for details.
|
|
510
|
+
|
|
511
|
+
## Links
|
|
512
|
+
|
|
513
|
+
- [Conductor OSS](https://github.com/conductor-oss/conductor)
|
|
514
|
+
- [Orkes Cloud](https://orkes.io)
|
|
515
|
+
- [Documentation](https://conductor-oss.org)
|
|
516
|
+
- [Python SDK](https://github.com/conductor-sdk/conductor-python)
|
|
517
|
+
- [Community Slack](https://join.slack.com/t/orkes-conductor/shared_invite/zt-2vdbx239s-Eacdyqya9giNLHfrCavfaA)
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# LLM Chat Workflow Example
|
|
5
|
+
#
|
|
6
|
+
# Demonstrates building a conversational AI workflow using the new Ruby DSL
|
|
7
|
+
# with LLM Chat Complete task and message history.
|
|
8
|
+
#
|
|
9
|
+
# New DSL Features Shown:
|
|
10
|
+
# - llm_chat() method with automatic message conversion
|
|
11
|
+
# - Pass messages as simple hashes instead of ChatMessage objects
|
|
12
|
+
#
|
|
13
|
+
# Usage:
|
|
14
|
+
# bundle exec ruby examples/agentic_workflows/llm_chat.rb
|
|
15
|
+
|
|
16
|
+
require_relative '../../lib/conductor'
|
|
17
|
+
|
|
18
|
+
LLM_PROVIDER = ENV.fetch('LLM_PROVIDER', 'openai')
|
|
19
|
+
LLM_MODEL = ENV.fetch('LLM_MODEL', 'gpt-4o-mini')
|
|
20
|
+
|
|
21
|
+
def create_chat_workflow(executor)
|
|
22
|
+
# Create workflow using the new Ruby-idiomatic DSL
|
|
23
|
+
Conductor.workflow :llm_chat_ruby, version: 1, executor: executor do
|
|
24
|
+
description 'Simple LLM chat workflow using new DSL'
|
|
25
|
+
|
|
26
|
+
# Chat completion task with messages as hashes (auto-converted)
|
|
27
|
+
# The DSL automatically converts hash messages to ChatMessage objects
|
|
28
|
+
chat = llm_chat :chat,
|
|
29
|
+
provider: LLM_PROVIDER,
|
|
30
|
+
model: LLM_MODEL,
|
|
31
|
+
messages: [
|
|
32
|
+
{
|
|
33
|
+
role: :system,
|
|
34
|
+
message: <<~MSG
|
|
35
|
+
You are a helpful assistant specializing in software development.
|
|
36
|
+
You provide clear, concise answers with code examples when appropriate.
|
|
37
|
+
Keep responses focused and practical.
|
|
38
|
+
MSG
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
role: :user,
|
|
42
|
+
message: '${workflow.input.user_message}'
|
|
43
|
+
}
|
|
44
|
+
],
|
|
45
|
+
temperature: 0.7,
|
|
46
|
+
max_tokens: 1024
|
|
47
|
+
|
|
48
|
+
# Set workflow output to the chat response
|
|
49
|
+
output response: chat[:result]
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def main
|
|
54
|
+
config = Conductor::Configuration.new
|
|
55
|
+
|
|
56
|
+
puts '=' * 70
|
|
57
|
+
puts 'LLM Chat Workflow Example (New DSL)'
|
|
58
|
+
puts '=' * 70
|
|
59
|
+
puts "Server: #{config.server_url}"
|
|
60
|
+
puts "LLM: #{LLM_PROVIDER}/#{LLM_MODEL}"
|
|
61
|
+
puts
|
|
62
|
+
|
|
63
|
+
clients = Conductor::Orkes::OrkesClients.new(config)
|
|
64
|
+
workflow_executor = clients.get_workflow_executor
|
|
65
|
+
|
|
66
|
+
# Create workflow with executor (required for register/execute)
|
|
67
|
+
workflow = create_chat_workflow(workflow_executor)
|
|
68
|
+
|
|
69
|
+
# Register workflow
|
|
70
|
+
workflow.register(overwrite: true)
|
|
71
|
+
puts "Registered workflow: #{workflow.name}"
|
|
72
|
+
|
|
73
|
+
# Test questions
|
|
74
|
+
questions = [
|
|
75
|
+
'What is the difference between a Hash and an Array in Ruby?',
|
|
76
|
+
'How do I handle exceptions in Ruby?',
|
|
77
|
+
'Explain Ruby blocks in one sentence.'
|
|
78
|
+
]
|
|
79
|
+
|
|
80
|
+
questions.each_with_index do |question, i|
|
|
81
|
+
puts "\n--- Question #{i + 1} ---"
|
|
82
|
+
puts "Q: #{question}"
|
|
83
|
+
|
|
84
|
+
# Execute using the workflow's execute method
|
|
85
|
+
result = workflow.execute(
|
|
86
|
+
input: { 'user_message' => question },
|
|
87
|
+
wait_for_seconds: 30
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
puts "A: #{result.output['response']}"
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
puts "\n#{'=' * 70}"
|
|
94
|
+
puts 'Chat workflow demo complete!'
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
if __FILE__ == $PROGRAM_NAME
|
|
98
|
+
begin
|
|
99
|
+
main
|
|
100
|
+
rescue Conductor::ApiError => e
|
|
101
|
+
puts "API Error: #{e.message}"
|
|
102
|
+
rescue StandardError => e
|
|
103
|
+
puts "Error: #{e.message}"
|
|
104
|
+
puts e.backtrace.first(3).join("\n")
|
|
105
|
+
end
|
|
106
|
+
end
|