ruby_llm-agents 1.0.0 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/app/controllers/concerns/ruby_llm/agents/paginatable.rb +9 -3
- data/app/controllers/concerns/ruby_llm/agents/sortable.rb +58 -0
- data/app/controllers/ruby_llm/agents/agents_controller.rb +59 -16
- data/app/controllers/ruby_llm/agents/dashboard_controller.rb +144 -20
- data/app/controllers/ruby_llm/agents/executions_controller.rb +13 -16
- data/app/controllers/ruby_llm/agents/workflows_controller.rb +279 -90
- data/app/helpers/ruby_llm/agents/application_helper.rb +100 -0
- data/app/mailers/ruby_llm/agents/alert_mailer.rb +84 -0
- data/app/mailers/ruby_llm/agents/application_mailer.rb +28 -0
- data/app/models/ruby_llm/agents/execution/analytics.rb +170 -20
- data/app/models/ruby_llm/agents/execution/scopes.rb +0 -31
- data/app/models/ruby_llm/agents/execution/workflow.rb +0 -129
- data/app/models/ruby_llm/agents/execution.rb +50 -14
- data/app/models/ruby_llm/agents/tenant/budgetable.rb +277 -0
- data/app/models/ruby_llm/agents/tenant/configurable.rb +135 -0
- data/app/models/ruby_llm/agents/tenant/trackable.rb +310 -0
- data/app/models/ruby_llm/agents/tenant.rb +146 -0
- data/app/models/ruby_llm/agents/tenant_budget.rb +12 -253
- data/app/services/ruby_llm/agents/agent_registry.rb +18 -12
- data/app/views/layouts/ruby_llm/agents/application.html.erb +72 -76
- data/app/views/ruby_llm/agents/agents/_agent.html.erb +0 -12
- data/app/views/ruby_llm/agents/agents/_sortable_header.html.erb +56 -0
- data/app/views/ruby_llm/agents/agents/_workflow.html.erb +5 -15
- data/app/views/ruby_llm/agents/agents/index.html.erb +271 -100
- data/app/views/ruby_llm/agents/agents/show.html.erb +1 -0
- data/app/views/ruby_llm/agents/alert_mailer/alert_notification.html.erb +107 -0
- data/app/views/ruby_llm/agents/alert_mailer/alert_notification.text.erb +18 -0
- data/app/views/ruby_llm/agents/api_configurations/show.html.erb +4 -1
- data/app/views/ruby_llm/agents/dashboard/_agent_comparison.html.erb +66 -359
- data/app/views/ruby_llm/agents/dashboard/_model_comparison.html.erb +56 -0
- data/app/views/ruby_llm/agents/dashboard/_model_cost_breakdown.html.erb +115 -0
- data/app/views/ruby_llm/agents/dashboard/_now_strip.html.erb +35 -60
- data/app/views/ruby_llm/agents/dashboard/_top_errors.html.erb +17 -6
- data/app/views/ruby_llm/agents/dashboard/index.html.erb +373 -72
- data/app/views/ruby_llm/agents/executions/_execution.html.erb +0 -1
- data/app/views/ruby_llm/agents/executions/_filters.html.erb +51 -39
- data/app/views/ruby_llm/agents/executions/_list.html.erb +53 -195
- data/app/views/ruby_llm/agents/executions/_workflow_summary.html.erb +5 -20
- data/app/views/ruby_llm/agents/executions/index.html.erb +7 -83
- data/app/views/ruby_llm/agents/executions/show.html.erb +10 -20
- data/app/views/ruby_llm/agents/shared/_agent_type_badge.html.erb +2 -1
- data/app/views/ruby_llm/agents/shared/_doc_link.html.erb +12 -0
- data/app/views/ruby_llm/agents/shared/_executions_table.html.erb +3 -15
- data/app/views/ruby_llm/agents/shared/_filter_dropdown.html.erb +1 -1
- data/app/views/ruby_llm/agents/shared/_select_dropdown.html.erb +1 -1
- data/app/views/ruby_llm/agents/shared/_sortable_header.html.erb +53 -0
- data/app/views/ruby_llm/agents/shared/_status_badge.html.erb +7 -0
- data/app/views/ruby_llm/agents/shared/_status_dot.html.erb +1 -1
- data/app/views/ruby_llm/agents/shared/_workflow_type_badge.html.erb +9 -35
- data/app/views/ruby_llm/agents/system_config/show.html.erb +4 -1
- data/app/views/ruby_llm/agents/tenants/index.html.erb +4 -1
- data/app/views/ruby_llm/agents/workflows/_step_performance.html.erb +7 -15
- data/app/views/ruby_llm/agents/workflows/_structure_dsl.html.erb +539 -0
- data/app/views/ruby_llm/agents/workflows/_workflow_diagram.html.erb +920 -0
- data/app/views/ruby_llm/agents/workflows/index.html.erb +179 -0
- data/app/views/ruby_llm/agents/workflows/show.html.erb +164 -139
- data/config/routes.rb +1 -1
- data/lib/generators/ruby_llm_agents/agent_generator.rb +6 -36
- data/lib/generators/ruby_llm_agents/background_remover_generator.rb +7 -37
- data/lib/generators/ruby_llm_agents/embedder_generator.rb +5 -38
- data/lib/generators/ruby_llm_agents/image_analyzer_generator.rb +7 -37
- data/lib/generators/ruby_llm_agents/image_editor_generator.rb +7 -37
- data/lib/generators/ruby_llm_agents/image_generator_generator.rb +8 -41
- data/lib/generators/ruby_llm_agents/image_pipeline_generator.rb +18 -46
- data/lib/generators/ruby_llm_agents/image_transformer_generator.rb +7 -37
- data/lib/generators/ruby_llm_agents/image_upscaler_generator.rb +7 -37
- data/lib/generators/ruby_llm_agents/image_variator_generator.rb +7 -37
- data/lib/generators/ruby_llm_agents/install_generator.rb +33 -56
- data/lib/generators/ruby_llm_agents/migrate_structure_generator.rb +480 -0
- data/lib/generators/ruby_llm_agents/multi_tenancy_generator.rb +42 -22
- data/lib/generators/ruby_llm_agents/restructure_generator.rb +2 -2
- data/lib/generators/ruby_llm_agents/speaker_generator.rb +8 -39
- data/lib/generators/ruby_llm_agents/templates/add_tenant_to_executions_migration.rb.tt +13 -2
- data/lib/generators/ruby_llm_agents/templates/agent.rb.tt +5 -8
- data/lib/generators/ruby_llm_agents/templates/application_agent.rb.tt +40 -42
- data/lib/generators/ruby_llm_agents/templates/application_background_remover.rb.tt +20 -22
- data/lib/generators/ruby_llm_agents/templates/application_embedder.rb.tt +24 -26
- data/lib/generators/ruby_llm_agents/templates/application_image_analyzer.rb.tt +20 -22
- data/lib/generators/ruby_llm_agents/templates/application_image_editor.rb.tt +19 -17
- data/lib/generators/ruby_llm_agents/templates/application_image_generator.rb.tt +31 -33
- data/lib/generators/ruby_llm_agents/templates/application_image_pipeline.rb.tt +125 -127
- data/lib/generators/ruby_llm_agents/templates/application_image_transformer.rb.tt +20 -18
- data/lib/generators/ruby_llm_agents/templates/application_image_upscaler.rb.tt +19 -17
- data/lib/generators/ruby_llm_agents/templates/application_image_variator.rb.tt +19 -17
- data/lib/generators/ruby_llm_agents/templates/application_speaker.rb.tt +38 -40
- data/lib/generators/ruby_llm_agents/templates/application_transcriber.rb.tt +42 -44
- data/lib/generators/ruby_llm_agents/templates/application_workflow.rb.tt +48 -0
- data/lib/generators/ruby_llm_agents/templates/background_remover.rb.tt +19 -21
- data/lib/generators/ruby_llm_agents/templates/create_tenant_budgets_migration.rb.tt +11 -0
- data/lib/generators/ruby_llm_agents/templates/create_tenants_migration.rb.tt +72 -0
- data/lib/generators/ruby_llm_agents/templates/embedder.rb.tt +19 -21
- data/lib/generators/ruby_llm_agents/templates/image_analyzer.rb.tt +20 -22
- data/lib/generators/ruby_llm_agents/templates/image_editor.rb.tt +15 -17
- data/lib/generators/ruby_llm_agents/templates/image_generator.rb.tt +25 -27
- data/lib/generators/ruby_llm_agents/templates/image_pipeline.rb.tt +19 -21
- data/lib/generators/ruby_llm_agents/templates/image_transformer.rb.tt +20 -22
- data/lib/generators/ruby_llm_agents/templates/image_upscaler.rb.tt +17 -19
- data/lib/generators/ruby_llm_agents/templates/image_variator.rb.tt +15 -17
- data/lib/generators/ruby_llm_agents/templates/rename_tenant_budgets_to_tenants_migration.rb.tt +34 -0
- data/lib/generators/ruby_llm_agents/templates/skills/AGENTS.md.tt +87 -24
- data/lib/generators/ruby_llm_agents/templates/skills/BACKGROUND_REMOVERS.md.tt +21 -27
- data/lib/generators/ruby_llm_agents/templates/skills/EMBEDDERS.md.tt +46 -54
- data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_ANALYZERS.md.tt +31 -39
- data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_EDITORS.md.tt +22 -28
- data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_GENERATORS.md.tt +53 -63
- data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_PIPELINES.md.tt +46 -56
- data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_TRANSFORMERS.md.tt +23 -31
- data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_UPSCALERS.md.tt +22 -30
- data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_VARIATORS.md.tt +23 -31
- data/lib/generators/ruby_llm_agents/templates/skills/SPEAKERS.md.tt +38 -46
- data/lib/generators/ruby_llm_agents/templates/skills/TOOLS.md.tt +7 -7
- data/lib/generators/ruby_llm_agents/templates/skills/TRANSCRIBERS.md.tt +59 -71
- data/lib/generators/ruby_llm_agents/templates/skills/WORKFLOWS.md.tt +274 -23
- data/lib/generators/ruby_llm_agents/templates/speaker.rb.tt +29 -31
- data/lib/generators/ruby_llm_agents/templates/transcriber.rb.tt +28 -30
- data/lib/generators/ruby_llm_agents/transcriber_generator.rb +10 -43
- data/lib/generators/ruby_llm_agents/upgrade_generator.rb +26 -0
- data/lib/ruby_llm/agents/core/configuration.rb +55 -43
- data/lib/ruby_llm/agents/core/llm_tenant.rb +60 -60
- data/lib/ruby_llm/agents/core/version.rb +1 -1
- data/lib/ruby_llm/agents/infrastructure/alert_manager.rb +26 -0
- data/lib/ruby_llm/agents/infrastructure/budget/config_resolver.rb +4 -2
- data/lib/ruby_llm/agents/pipeline.rb +69 -0
- data/lib/ruby_llm/agents/workflow/approval.rb +205 -0
- data/lib/ruby_llm/agents/workflow/approval_store.rb +179 -0
- data/lib/ruby_llm/agents/workflow/dsl/executor.rb +467 -0
- data/lib/ruby_llm/agents/workflow/dsl/input_schema.rb +244 -0
- data/lib/ruby_llm/agents/workflow/dsl/iteration_executor.rb +289 -0
- data/lib/ruby_llm/agents/workflow/dsl/parallel_group.rb +107 -0
- data/lib/ruby_llm/agents/workflow/dsl/route_builder.rb +150 -0
- data/lib/ruby_llm/agents/workflow/dsl/schedule_helpers.rb +187 -0
- data/lib/ruby_llm/agents/workflow/dsl/step_config.rb +352 -0
- data/lib/ruby_llm/agents/workflow/dsl/step_executor.rb +415 -0
- data/lib/ruby_llm/agents/workflow/dsl/wait_config.rb +257 -0
- data/lib/ruby_llm/agents/workflow/dsl/wait_executor.rb +317 -0
- data/lib/ruby_llm/agents/workflow/dsl.rb +576 -0
- data/lib/ruby_llm/agents/workflow/instrumentation.rb +2 -7
- data/lib/ruby_llm/agents/workflow/notifiers/base.rb +117 -0
- data/lib/ruby_llm/agents/workflow/notifiers/email.rb +117 -0
- data/lib/ruby_llm/agents/workflow/notifiers/slack.rb +180 -0
- data/lib/ruby_llm/agents/workflow/notifiers/webhook.rb +121 -0
- data/lib/ruby_llm/agents/workflow/notifiers.rb +70 -0
- data/lib/ruby_llm/agents/workflow/orchestrator.rb +190 -23
- data/lib/ruby_llm/agents/workflow/result.rb +202 -0
- data/lib/ruby_llm/agents/workflow/throttle_manager.rb +206 -0
- data/lib/ruby_llm/agents/workflow/wait_result.rb +213 -0
- metadata +43 -6
- data/app/views/ruby_llm/agents/dashboard/_execution_item.html.erb +0 -66
- data/lib/ruby_llm/agents/workflow/parallel.rb +0 -299
- data/lib/ruby_llm/agents/workflow/pipeline.rb +0 -306
- data/lib/ruby_llm/agents/workflow/router.rb +0 -429
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Workflows
|
|
2
2
|
|
|
3
3
|
This directory contains workflow orchestration classes that compose multiple agents. Workflows provide patterns for sequential, parallel, and conditional agent execution.
|
|
4
4
|
|
|
@@ -6,18 +6,254 @@ This directory contains workflow orchestration classes that compose multiple age
|
|
|
6
6
|
|
|
7
7
|
| Type | Class | Description |
|
|
8
8
|
|------|-------|-------------|
|
|
9
|
-
|
|
|
10
|
-
|
|
|
11
|
-
|
|
|
9
|
+
| **DSL Workflow** | `Workflow` | Declarative DSL for mixed sequential/parallel/routing |
|
|
10
|
+
| Pipeline | `Workflow::Pipeline` | Legacy: Sequential execution, data flows between steps |
|
|
11
|
+
| Parallel | `Workflow::Parallel` | Legacy: Concurrent execution with aggregation |
|
|
12
|
+
| Router | `Workflow::Router` | Legacy: Conditional dispatch based on classification |
|
|
12
13
|
|
|
13
|
-
##
|
|
14
|
+
## Declarative DSL Workflows (Recommended)
|
|
15
|
+
|
|
16
|
+
The new declarative DSL provides a clean, expressive syntax for defining workflows with minimal boilerplate. It supports sequential steps, parallel execution, conditional routing, and input validation all in one workflow class.
|
|
17
|
+
|
|
18
|
+
### Minimal Workflow
|
|
19
|
+
|
|
20
|
+
```ruby
|
|
21
|
+
module Workflows
|
|
22
|
+
class SimpleWorkflow < RubyLLM::Agents::Workflow
|
|
23
|
+
step :fetch, FetcherAgent
|
|
24
|
+
step :process, ProcessorAgent
|
|
25
|
+
step :save, SaverAgent
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Usage
|
|
30
|
+
result = Workflows::SimpleWorkflow.call(order_id: "ORD-123")
|
|
31
|
+
result.success? # => true
|
|
32
|
+
result.content # => final step output
|
|
33
|
+
result.steps[:process] # => ProcessorAgent result
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
### Full-Featured Workflow
|
|
37
|
+
|
|
38
|
+
```ruby
|
|
39
|
+
module Workflows
|
|
40
|
+
class OrderWorkflow < RubyLLM::Agents::Workflow
|
|
41
|
+
description "Process customer orders end-to-end"
|
|
42
|
+
version "2.0"
|
|
43
|
+
timeout 300
|
|
44
|
+
max_cost 2.00
|
|
45
|
+
|
|
46
|
+
# Input validation with defaults
|
|
47
|
+
input do
|
|
48
|
+
required :order_id, String
|
|
49
|
+
required :user_id, Integer
|
|
50
|
+
optional :priority, String, default: "normal"
|
|
51
|
+
optional :expedited, :boolean, default: false
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Sequential steps
|
|
55
|
+
step :fetch, FetcherAgent, timeout: 30
|
|
56
|
+
step :validate, ValidatorAgent
|
|
57
|
+
|
|
58
|
+
# Conditional routing based on previous step result
|
|
59
|
+
step :process, on: -> { validate.tier } do |route|
|
|
60
|
+
route.premium PremiumProcessorAgent
|
|
61
|
+
route.standard StandardProcessorAgent
|
|
62
|
+
route.default BasicProcessorAgent
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Parallel execution block
|
|
66
|
+
parallel do
|
|
67
|
+
step :analyze, AnalyzerAgent
|
|
68
|
+
step :summarize, SummarizerAgent
|
|
69
|
+
step :notify, NotifierAgent
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Conditional step execution
|
|
73
|
+
step :expedite, ExpediteAgent, if: :expedited?
|
|
74
|
+
|
|
75
|
+
private
|
|
76
|
+
|
|
77
|
+
def expedited?
|
|
78
|
+
input.expedited == true
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### DSL Reference
|
|
85
|
+
|
|
86
|
+
#### Input Schema
|
|
87
|
+
|
|
88
|
+
Define required and optional inputs with type validation:
|
|
89
|
+
|
|
90
|
+
```ruby
|
|
91
|
+
input do
|
|
92
|
+
required :order_id, String
|
|
93
|
+
required :amount, Numeric
|
|
94
|
+
optional :priority, String, default: "normal"
|
|
95
|
+
optional :debug, :boolean, default: false
|
|
96
|
+
|
|
97
|
+
# With enum validation
|
|
98
|
+
optional :status, String, in: %w[pending active completed]
|
|
99
|
+
end
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
#### Step Options
|
|
103
|
+
|
|
104
|
+
```ruby
|
|
105
|
+
# Basic step
|
|
106
|
+
step :name, AgentClass
|
|
107
|
+
|
|
108
|
+
# With description
|
|
109
|
+
step :validate, ValidatorAgent, "Validates order data"
|
|
110
|
+
|
|
111
|
+
# With timeout (seconds)
|
|
112
|
+
step :fetch, FetcherAgent, timeout: 30
|
|
113
|
+
|
|
114
|
+
# Optional step (workflow continues on failure)
|
|
115
|
+
step :enrich, EnricherAgent, optional: true
|
|
116
|
+
|
|
117
|
+
# With default value on failure
|
|
118
|
+
step :lookup, LookupAgent, optional: true, default: { found: false }
|
|
119
|
+
|
|
120
|
+
# Conditional execution
|
|
121
|
+
step :notify, NotifierAgent, if: :should_notify?
|
|
122
|
+
step :skip_this, SkipAgent, unless: :condition_met?
|
|
123
|
+
|
|
124
|
+
# With retry
|
|
125
|
+
step :api_call, ApiAgent, retry: 3
|
|
126
|
+
step :api_call, ApiAgent, retry: { max: 3, delay: 2, backoff: :exponential }
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
#### Conditional Routing
|
|
130
|
+
|
|
131
|
+
Route to different agents based on a value:
|
|
132
|
+
|
|
133
|
+
```ruby
|
|
134
|
+
# Route based on lambda returning a value
|
|
135
|
+
step :process, on: -> { validate.tier } do |route|
|
|
136
|
+
route.premium PremiumAgent
|
|
137
|
+
route.standard StandardAgent
|
|
138
|
+
route.default FallbackAgent # Required fallback
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# With custom input mapping per route
|
|
142
|
+
step :handle, on: -> { classify.type } do |route|
|
|
143
|
+
route.billing BillingAgent, input: -> { { account: input.account_id } }
|
|
144
|
+
route.support SupportAgent, input: -> { { ticket: input.ticket_id } }
|
|
145
|
+
route.default GeneralAgent
|
|
146
|
+
end
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
#### Parallel Execution
|
|
150
|
+
|
|
151
|
+
Execute steps concurrently:
|
|
152
|
+
|
|
153
|
+
```ruby
|
|
154
|
+
# Anonymous parallel group
|
|
155
|
+
parallel do
|
|
156
|
+
step :sentiment, SentimentAgent
|
|
157
|
+
step :keywords, KeywordAgent
|
|
158
|
+
step :entities, EntityAgent
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# Named parallel group with options
|
|
162
|
+
parallel :analysis, fail_fast: true, concurrency: 2 do
|
|
163
|
+
step :deep_analyze, DeepAnalyzer
|
|
164
|
+
step :quick_scan, QuickScanner
|
|
165
|
+
end
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
#### Lifecycle Hooks
|
|
169
|
+
|
|
170
|
+
```ruby
|
|
171
|
+
class MyWorkflow < RubyLLM::Agents::Workflow
|
|
172
|
+
step :process, ProcessorAgent
|
|
173
|
+
step :save, SaverAgent
|
|
174
|
+
|
|
175
|
+
# Workflow-level hooks
|
|
176
|
+
before_workflow { Rails.logger.info("Starting workflow") }
|
|
177
|
+
after_workflow { Rails.logger.info("Workflow complete") }
|
|
178
|
+
|
|
179
|
+
# Step-level hooks
|
|
180
|
+
before_step(:process) { |context| log_step_start(:process) }
|
|
181
|
+
after_step(:process) { |result, duration_ms| log_step_end(:process, duration_ms) }
|
|
182
|
+
|
|
183
|
+
# Error hooks
|
|
184
|
+
on_step_failure(:process) { |error, context| handle_error(error) }
|
|
185
|
+
end
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
#### Accessing Step Results
|
|
189
|
+
|
|
190
|
+
Within the workflow, access previous step results:
|
|
191
|
+
|
|
192
|
+
```ruby
|
|
193
|
+
step :validate, ValidatorAgent
|
|
194
|
+
|
|
195
|
+
# In routing lambda
|
|
196
|
+
step :process, on: -> { validate.tier } do |route|
|
|
197
|
+
# validate.tier returns validate step's result[:tier]
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
# In condition methods
|
|
201
|
+
def should_notify?
|
|
202
|
+
validate.valid? && input.callback_url.present?
|
|
203
|
+
end
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
### Workflow Result API
|
|
207
|
+
|
|
208
|
+
```ruby
|
|
209
|
+
result = MyWorkflow.call(order_id: "123")
|
|
210
|
+
|
|
211
|
+
# Status
|
|
212
|
+
result.success? # All steps succeeded
|
|
213
|
+
result.partial? # Some optional steps failed
|
|
214
|
+
result.error? # Required step failed
|
|
215
|
+
result.status # "success", "partial", or "error"
|
|
216
|
+
|
|
217
|
+
# Content
|
|
218
|
+
result.content # Final step output
|
|
219
|
+
|
|
220
|
+
# Step results
|
|
221
|
+
result.steps # Hash of all step results
|
|
222
|
+
result.steps[:validate] # Specific step result
|
|
223
|
+
result.steps[:validate].content # Step output content
|
|
224
|
+
|
|
225
|
+
# Metrics
|
|
226
|
+
result.total_cost # Combined cost of all steps
|
|
227
|
+
result.input_tokens # Total input tokens
|
|
228
|
+
result.output_tokens # Total output tokens
|
|
229
|
+
result.duration_ms # Total workflow duration
|
|
230
|
+
|
|
231
|
+
# Errors
|
|
232
|
+
result.errors # Hash of step errors
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
### Dry Run / Validation
|
|
236
|
+
|
|
237
|
+
Validate workflow configuration without executing:
|
|
238
|
+
|
|
239
|
+
```ruby
|
|
240
|
+
validation = MyWorkflow.dry_run(order_id: "123")
|
|
241
|
+
|
|
242
|
+
validation[:valid] # true/false
|
|
243
|
+
validation[:input_errors] # Array of validation errors
|
|
244
|
+
validation[:steps] # List of step names
|
|
245
|
+
validation[:agents] # List of agent classes
|
|
246
|
+
validation[:parallel_groups] # Parallel group info
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
## Creating Workflows (Legacy Patterns)
|
|
14
250
|
|
|
15
251
|
### Pipeline (Sequential)
|
|
16
252
|
|
|
17
253
|
Execute agents in order, passing each step's output to the next:
|
|
18
254
|
|
|
19
255
|
```ruby
|
|
20
|
-
module
|
|
256
|
+
module Workflows
|
|
21
257
|
class ContentPipeline < RubyLLM::Agents::Workflow::Pipeline
|
|
22
258
|
version "1.0"
|
|
23
259
|
timeout 120
|
|
@@ -35,7 +271,7 @@ end
|
|
|
35
271
|
Execute multiple agents simultaneously:
|
|
36
272
|
|
|
37
273
|
```ruby
|
|
38
|
-
module
|
|
274
|
+
module Workflows
|
|
39
275
|
class ReviewAnalyzer < RubyLLM::Agents::Workflow::Parallel
|
|
40
276
|
version "1.0"
|
|
41
277
|
|
|
@@ -59,7 +295,7 @@ end
|
|
|
59
295
|
Route to different agents based on classification:
|
|
60
296
|
|
|
61
297
|
```ruby
|
|
62
|
-
module
|
|
298
|
+
module Workflows
|
|
63
299
|
class SupportRouter < RubyLLM::Agents::Workflow::Router
|
|
64
300
|
version "1.0"
|
|
65
301
|
classifier ClassificationAgent
|
|
@@ -145,7 +381,7 @@ end
|
|
|
145
381
|
### Basic Execution
|
|
146
382
|
|
|
147
383
|
```ruby
|
|
148
|
-
result =
|
|
384
|
+
result = Workflows::ContentPipeline.call(text: "raw input")
|
|
149
385
|
|
|
150
386
|
result.success? # All steps succeeded
|
|
151
387
|
result.partial? # Some steps succeeded
|
|
@@ -158,7 +394,7 @@ result.duration_ms # Total duration
|
|
|
158
394
|
### Pipeline Results
|
|
159
395
|
|
|
160
396
|
```ruby
|
|
161
|
-
result =
|
|
397
|
+
result = Workflows::ContentPipeline.call(text: "input")
|
|
162
398
|
|
|
163
399
|
result.steps # Hash of all step results
|
|
164
400
|
result.steps[:extract].content # Specific step output
|
|
@@ -168,7 +404,7 @@ result.errors # Hash of step errors
|
|
|
168
404
|
### Parallel Results
|
|
169
405
|
|
|
170
406
|
```ruby
|
|
171
|
-
result =
|
|
407
|
+
result = Workflows::ReviewAnalyzer.call(text: "Great product!")
|
|
172
408
|
|
|
173
409
|
result.branches # Hash of branch results
|
|
174
410
|
result.branches[:sentiment].content # Specific branch output
|
|
@@ -177,7 +413,7 @@ result.branches[:sentiment].content # Specific branch output
|
|
|
177
413
|
### Router Results
|
|
178
414
|
|
|
179
415
|
```ruby
|
|
180
|
-
result =
|
|
416
|
+
result = Workflows::SupportRouter.call(question: "How do I pay?")
|
|
181
417
|
|
|
182
418
|
result.classification # What route was selected
|
|
183
419
|
result.route # Which agent handled it
|
|
@@ -191,7 +427,7 @@ result.content # Response from routed agent
|
|
|
191
427
|
Transform data between pipeline steps:
|
|
192
428
|
|
|
193
429
|
```ruby
|
|
194
|
-
module
|
|
430
|
+
module Workflows
|
|
195
431
|
class TransformPipeline < RubyLLM::Agents::Workflow::Pipeline
|
|
196
432
|
step :analyze, agent: AnalyzerAgent
|
|
197
433
|
step :enrich, agent: EnricherAgent
|
|
@@ -212,7 +448,7 @@ end
|
|
|
212
448
|
Combine parallel results:
|
|
213
449
|
|
|
214
450
|
```ruby
|
|
215
|
-
module
|
|
451
|
+
module Workflows
|
|
216
452
|
class SafetyChecker < RubyLLM::Agents::Workflow::Parallel
|
|
217
453
|
branch :toxicity, agent: ToxicityAgent
|
|
218
454
|
branch :spam, agent: SpamAgent
|
|
@@ -233,7 +469,7 @@ end
|
|
|
233
469
|
Handle step failures:
|
|
234
470
|
|
|
235
471
|
```ruby
|
|
236
|
-
module
|
|
472
|
+
module Workflows
|
|
237
473
|
class ResilientPipeline < RubyLLM::Agents::Workflow::Pipeline
|
|
238
474
|
step :primary, agent: PrimaryAgent
|
|
239
475
|
step :backup, agent: BackupAgent, optional: true
|
|
@@ -253,7 +489,7 @@ end
|
|
|
253
489
|
Stop workflow if cost exceeds threshold:
|
|
254
490
|
|
|
255
491
|
```ruby
|
|
256
|
-
module
|
|
492
|
+
module Workflows
|
|
257
493
|
class BudgetedWorkflow < RubyLLM::Agents::Workflow::Pipeline
|
|
258
494
|
max_cost 1.00 # Stop if workflow exceeds $1.00
|
|
259
495
|
|
|
@@ -266,7 +502,7 @@ end
|
|
|
266
502
|
## Testing Workflows
|
|
267
503
|
|
|
268
504
|
```ruby
|
|
269
|
-
RSpec.describe
|
|
505
|
+
RSpec.describe Workflows::ContentPipeline do
|
|
270
506
|
describe ".call" do
|
|
271
507
|
it "processes content through all steps" do
|
|
272
508
|
result = described_class.call(text: "test input")
|
|
@@ -289,12 +525,27 @@ end
|
|
|
289
525
|
|
|
290
526
|
## Best Practices
|
|
291
527
|
|
|
292
|
-
|
|
293
|
-
|
|
528
|
+
### General
|
|
529
|
+
|
|
530
|
+
1. **Version your workflows** - Track changes for debugging and rollback
|
|
531
|
+
2. **Set timeouts** - Prevent runaway workflows at both workflow and step level
|
|
294
532
|
3. **Use max_cost** - Control spending on expensive operations
|
|
295
|
-
4. **Handle errors gracefully** - Use optional steps and error handlers
|
|
533
|
+
4. **Handle errors gracefully** - Use optional steps, defaults, and error handlers
|
|
296
534
|
5. **Keep workflows focused** - One workflow, one purpose
|
|
297
535
|
6. **Test step interactions** - Unit test agents, integration test workflows
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
536
|
+
|
|
537
|
+
### DSL Workflows (Recommended)
|
|
538
|
+
|
|
539
|
+
1. **Use input schemas** - Validate inputs early with `required`/`optional` declarations
|
|
540
|
+
2. **Prefer DSL over legacy patterns** - The new DSL is more flexible and composable
|
|
541
|
+
3. **Use parallel for independent operations** - Group unrelated steps for concurrent execution
|
|
542
|
+
4. **Use routing for classification** - Clean syntax for conditional agent selection
|
|
543
|
+
5. **Leverage step result access** - Use `step_name.field` syntax in conditions/routing
|
|
544
|
+
6. **Add lifecycle hooks** - Log, track, and handle errors at appropriate points
|
|
545
|
+
7. **Use dry_run for validation** - Check workflow configuration before execution
|
|
546
|
+
|
|
547
|
+
### Legacy Patterns
|
|
548
|
+
|
|
549
|
+
1. **Use Pipeline for sequential data processing** - Data flows naturally between steps
|
|
550
|
+
2. **Use Parallel for fan-out operations** - Independent operations with aggregation
|
|
551
|
+
3. **Use Router for classification-based dispatch** - When route selection depends on LLM classification
|
|
@@ -1,56 +1,54 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
module
|
|
4
|
-
module Audio
|
|
3
|
+
module Audio
|
|
5
4
|
<%- if class_name.include?("::") -%>
|
|
6
5
|
<%- class_name.split("::")[0..-2].each_with_index do |mod, i| -%>
|
|
7
|
-
<%= " " * (i +
|
|
6
|
+
<%= " " * (i + 1) %>module <%= mod %>
|
|
8
7
|
<%- end -%>
|
|
9
|
-
<%= " " *
|
|
8
|
+
<%= " " * class_name.split("::").length %>class <%= class_name.split("::").last %>Speaker < ApplicationSpeaker
|
|
10
9
|
<%- else -%>
|
|
11
|
-
|
|
10
|
+
class <%= class_name %>Speaker < ApplicationSpeaker
|
|
12
11
|
<%- end -%>
|
|
13
|
-
|
|
14
|
-
|
|
12
|
+
# Provider configuration
|
|
13
|
+
provider :<%= options[:provider] %>
|
|
15
14
|
<% if options[:model] -%>
|
|
16
|
-
|
|
15
|
+
model "<%= options[:model] %>"
|
|
17
16
|
<% end -%>
|
|
18
|
-
|
|
17
|
+
voice "<%= options[:voice] %>"
|
|
19
18
|
<% if options[:speed] != 1.0 -%>
|
|
20
|
-
|
|
19
|
+
speed <%= options[:speed] %>
|
|
21
20
|
<% end -%>
|
|
22
21
|
<% if options[:format] != "mp3" -%>
|
|
23
|
-
|
|
22
|
+
output_format :<%= options[:format] %>
|
|
24
23
|
<% end -%>
|
|
25
24
|
<% if options[:cache] -%>
|
|
26
25
|
|
|
27
|
-
|
|
28
|
-
|
|
26
|
+
# Caching
|
|
27
|
+
cache_for <%= options[:cache] %>
|
|
29
28
|
<% end -%>
|
|
30
29
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
30
|
+
# Optional: Custom pronunciation lexicon
|
|
31
|
+
# Define how specific words should be pronounced
|
|
32
|
+
#
|
|
33
|
+
# lexicon do
|
|
34
|
+
# pronounce 'API', 'A P I'
|
|
35
|
+
# pronounce 'SQL', 'sequel'
|
|
36
|
+
# end
|
|
38
37
|
<% if options[:provider].to_s == "elevenlabs" -%>
|
|
39
38
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
39
|
+
# ElevenLabs voice settings
|
|
40
|
+
# voice_settings do
|
|
41
|
+
# stability 0.5
|
|
42
|
+
# similarity_boost 0.75
|
|
43
|
+
# style 0.5
|
|
44
|
+
# speaker_boost true
|
|
45
|
+
# end
|
|
47
46
|
<% end -%>
|
|
48
47
|
<%- if class_name.include?("::") -%>
|
|
49
|
-
<%-
|
|
50
|
-
<%= " " * (class_name.split("::").length
|
|
48
|
+
<%- class_name.split("::").length.times do |i| -%>
|
|
49
|
+
<%= " " * (class_name.split("::").length - i) %>end
|
|
51
50
|
<%- end -%>
|
|
52
51
|
<%- else -%>
|
|
53
|
-
end
|
|
54
|
-
<%- end -%>
|
|
55
52
|
end
|
|
53
|
+
<%- end -%>
|
|
56
54
|
end
|
|
@@ -1,51 +1,49 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
module
|
|
4
|
-
module Audio
|
|
3
|
+
module Audio
|
|
5
4
|
<%- if class_name.include?("::") -%>
|
|
6
5
|
<%- class_name.split("::")[0..-2].each_with_index do |mod, i| -%>
|
|
7
|
-
<%= " " * (i +
|
|
6
|
+
<%= " " * (i + 1) %>module <%= mod %>
|
|
8
7
|
<%- end -%>
|
|
9
|
-
<%= " " *
|
|
8
|
+
<%= " " * class_name.split("::").length %>class <%= class_name.split("::").last %>Transcriber < ApplicationTranscriber
|
|
10
9
|
<%- else -%>
|
|
11
|
-
|
|
10
|
+
class <%= class_name %>Transcriber < ApplicationTranscriber
|
|
12
11
|
<%- end -%>
|
|
13
|
-
|
|
14
|
-
|
|
12
|
+
# Model configuration
|
|
13
|
+
model "<%= options[:model] %>"
|
|
15
14
|
<% if options[:language] -%>
|
|
16
|
-
|
|
15
|
+
language "<%= options[:language] %>"
|
|
17
16
|
<% end -%>
|
|
18
17
|
<% if options[:output_format] != "text" -%>
|
|
19
|
-
|
|
18
|
+
output_format :<%= options[:output_format] %>
|
|
20
19
|
<% end -%>
|
|
21
20
|
<% if options[:cache] -%>
|
|
22
21
|
|
|
23
|
-
|
|
24
|
-
|
|
22
|
+
# Caching
|
|
23
|
+
cache_for <%= options[:cache] %>
|
|
25
24
|
<% end -%>
|
|
26
25
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
26
|
+
# Optional: Provide context to improve accuracy
|
|
27
|
+
# Override this method to give the model hints about the audio content
|
|
28
|
+
#
|
|
29
|
+
# def prompt
|
|
30
|
+
# "Technical discussion about Ruby programming"
|
|
31
|
+
# end
|
|
33
32
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
33
|
+
# Optional: Clean up transcription text
|
|
34
|
+
# Override this method to post-process the transcribed text
|
|
35
|
+
#
|
|
36
|
+
# def postprocess_text(text)
|
|
37
|
+
# text
|
|
38
|
+
# .gsub(/\bum\b/i, '') # Remove filler words
|
|
39
|
+
# .gsub(/\buh\b/i, '')
|
|
40
|
+
# .squeeze(' ') # Remove extra spaces
|
|
41
|
+
# end
|
|
43
42
|
<%- if class_name.include?("::") -%>
|
|
44
|
-
<%-
|
|
45
|
-
<%= " " * (class_name.split("::").length
|
|
43
|
+
<%- class_name.split("::").length.times do |i| -%>
|
|
44
|
+
<%= " " * (class_name.split("::").length - i) %>end
|
|
46
45
|
<%- end -%>
|
|
47
46
|
<%- else -%>
|
|
48
|
-
end
|
|
49
|
-
<%- end -%>
|
|
50
47
|
end
|
|
48
|
+
<%- end -%>
|
|
51
49
|
end
|
|
@@ -7,12 +7,11 @@ module RubyLlmAgents
|
|
|
7
7
|
#
|
|
8
8
|
# Usage:
|
|
9
9
|
# rails generate ruby_llm_agents:transcriber Meeting
|
|
10
|
-
# rails generate ruby_llm_agents:transcriber
|
|
11
|
-
# rails generate ruby_llm_agents:transcriber
|
|
12
|
-
# rails generate ruby_llm_agents:transcriber Meeting --root=ai
|
|
10
|
+
# rails generate ruby_llm_agents:transcriber Interview --model whisper-1
|
|
11
|
+
# rails generate ruby_llm_agents:transcriber Podcast --language en
|
|
13
12
|
#
|
|
14
13
|
# This will create:
|
|
15
|
-
# - app/
|
|
14
|
+
# - app/agents/audio/meeting_transcriber.rb
|
|
16
15
|
#
|
|
17
16
|
class TranscriberGenerator < ::Rails::Generators::NamedBase
|
|
18
17
|
source_root File.expand_path("templates", __dir__)
|
|
@@ -25,48 +24,36 @@ module RubyLlmAgents
|
|
|
25
24
|
desc: "Output format (text, srt, vtt, json)"
|
|
26
25
|
class_option :cache, type: :string, default: nil,
|
|
27
26
|
desc: "Cache TTL (e.g., '30.days')"
|
|
28
|
-
class_option :root,
|
|
29
|
-
type: :string,
|
|
30
|
-
default: nil,
|
|
31
|
-
desc: "Root directory name (default: uses config or 'llm')"
|
|
32
|
-
class_option :namespace,
|
|
33
|
-
type: :string,
|
|
34
|
-
default: nil,
|
|
35
|
-
desc: "Root namespace (default: camelized root or config)"
|
|
36
27
|
|
|
37
28
|
def ensure_base_class_and_skill_file
|
|
38
|
-
|
|
39
|
-
@audio_namespace = "#{root_namespace}::Audio"
|
|
40
|
-
transcribers_dir = "app/#{root_directory}/audio/transcribers"
|
|
29
|
+
audio_dir = "app/agents/audio"
|
|
41
30
|
|
|
42
31
|
# Create directory if needed
|
|
43
|
-
empty_directory
|
|
32
|
+
empty_directory audio_dir
|
|
44
33
|
|
|
45
34
|
# Create base class if it doesn't exist
|
|
46
|
-
base_class_path = "#{
|
|
35
|
+
base_class_path = "#{audio_dir}/application_transcriber.rb"
|
|
47
36
|
unless File.exist?(File.join(destination_root, base_class_path))
|
|
48
37
|
template "application_transcriber.rb.tt", base_class_path
|
|
49
38
|
end
|
|
50
39
|
|
|
51
40
|
# Create skill file if it doesn't exist
|
|
52
|
-
skill_file_path = "#{
|
|
41
|
+
skill_file_path = "#{audio_dir}/TRANSCRIBERS.md"
|
|
53
42
|
unless File.exist?(File.join(destination_root, skill_file_path))
|
|
54
43
|
template "skills/TRANSCRIBERS.md.tt", skill_file_path
|
|
55
44
|
end
|
|
56
45
|
end
|
|
57
46
|
|
|
58
47
|
def create_transcriber_file
|
|
59
|
-
# Support nested paths: "interview/meeting" -> "app/
|
|
60
|
-
@root_namespace = root_namespace
|
|
61
|
-
@audio_namespace = "#{root_namespace}::Audio"
|
|
48
|
+
# Support nested paths: "interview/meeting" -> "app/agents/audio/interview/meeting_transcriber.rb"
|
|
62
49
|
transcriber_path = name.underscore
|
|
63
|
-
template "transcriber.rb.tt", "app
|
|
50
|
+
template "transcriber.rb.tt", "app/agents/audio/#{transcriber_path}_transcriber.rb"
|
|
64
51
|
end
|
|
65
52
|
|
|
66
53
|
def show_usage
|
|
67
54
|
# Build full class name from path
|
|
68
55
|
transcriber_class_name = name.split("/").map(&:camelize).join("::")
|
|
69
|
-
full_class_name = "
|
|
56
|
+
full_class_name = "Audio::#{transcriber_class_name}Transcriber"
|
|
70
57
|
say ""
|
|
71
58
|
say "Transcriber #{full_class_name} created!", :green
|
|
72
59
|
say ""
|
|
@@ -83,25 +70,5 @@ module RubyLlmAgents
|
|
|
83
70
|
say " result.vtt # VTT format"
|
|
84
71
|
say ""
|
|
85
72
|
end
|
|
86
|
-
|
|
87
|
-
private
|
|
88
|
-
|
|
89
|
-
def root_directory
|
|
90
|
-
@root_directory ||= options[:root] || RubyLLM::Agents.configuration.root_directory
|
|
91
|
-
end
|
|
92
|
-
|
|
93
|
-
def root_namespace
|
|
94
|
-
@root_namespace ||= options[:namespace] || camelize(root_directory)
|
|
95
|
-
end
|
|
96
|
-
|
|
97
|
-
def camelize(str)
|
|
98
|
-
# Handle special cases for common abbreviations
|
|
99
|
-
return "AI" if str.downcase == "ai"
|
|
100
|
-
return "ML" if str.downcase == "ml"
|
|
101
|
-
return "LLM" if str.downcase == "llm"
|
|
102
|
-
|
|
103
|
-
# Standard camelization
|
|
104
|
-
str.split(/[-_]/).map(&:capitalize).join
|
|
105
|
-
end
|
|
106
73
|
end
|
|
107
74
|
end
|