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
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
module Agents
|
|
5
|
+
class Workflow
|
|
6
|
+
module DSL
|
|
7
|
+
# Helper methods for scheduling wait_until time calculations
|
|
8
|
+
#
|
|
9
|
+
# These methods can be used within workflow definitions to create
|
|
10
|
+
# dynamic scheduling logic for wait_until time: expressions.
|
|
11
|
+
#
|
|
12
|
+
# @example Using in a workflow
|
|
13
|
+
# class ReportWorkflow < RubyLLM::Agents::Workflow
|
|
14
|
+
# include ScheduleHelpers
|
|
15
|
+
#
|
|
16
|
+
# step :generate, ReportAgent
|
|
17
|
+
# wait_until time: -> { next_weekday_at(9, 0) }
|
|
18
|
+
# step :send, EmailAgent
|
|
19
|
+
# end
|
|
20
|
+
#
|
|
21
|
+
# @api public
|
|
22
|
+
module ScheduleHelpers
|
|
23
|
+
# Returns the next occurrence of a weekday (Mon-Fri) at the specified time
|
|
24
|
+
#
|
|
25
|
+
# @param hour [Integer] Hour (0-23)
|
|
26
|
+
# @param minute [Integer] Minute (0-59)
|
|
27
|
+
# @param timezone [String, nil] Timezone name (uses system timezone if nil)
|
|
28
|
+
# @return [Time]
|
|
29
|
+
def next_weekday_at(hour, minute, timezone: nil)
|
|
30
|
+
now = current_time(timezone)
|
|
31
|
+
target = build_time(now, hour, minute, timezone)
|
|
32
|
+
|
|
33
|
+
# If target time has passed today or it's a weekend, find next weekday
|
|
34
|
+
if target <= now || weekend?(target)
|
|
35
|
+
target = advance_to_next_weekday(target)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
target
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Returns the start of the next hour
|
|
42
|
+
#
|
|
43
|
+
# @param timezone [String, nil] Timezone name
|
|
44
|
+
# @return [Time]
|
|
45
|
+
def next_hour(timezone: nil)
|
|
46
|
+
now = current_time(timezone)
|
|
47
|
+
Time.new(now.year, now.month, now.day, now.hour + 1, 0, 0, now.utc_offset)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Returns tomorrow at the specified time
|
|
51
|
+
#
|
|
52
|
+
# @param hour [Integer] Hour (0-23)
|
|
53
|
+
# @param minute [Integer] Minute (0-59)
|
|
54
|
+
# @param timezone [String, nil] Timezone name
|
|
55
|
+
# @return [Time]
|
|
56
|
+
def tomorrow_at(hour, minute, timezone: nil)
|
|
57
|
+
now = current_time(timezone)
|
|
58
|
+
tomorrow = now + 86_400 # Add one day in seconds
|
|
59
|
+
build_time(tomorrow, hour, minute, timezone)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Returns the next available time within business hours
|
|
63
|
+
#
|
|
64
|
+
# Business hours default to Mon-Fri, 9am-5pm.
|
|
65
|
+
#
|
|
66
|
+
# @param start_hour [Integer] Business day start hour (default: 9)
|
|
67
|
+
# @param end_hour [Integer] Business day end hour (default: 17)
|
|
68
|
+
# @param timezone [String, nil] Timezone name
|
|
69
|
+
# @return [Time]
|
|
70
|
+
def in_business_hours(start_hour: 9, end_hour: 17, timezone: nil)
|
|
71
|
+
now = current_time(timezone)
|
|
72
|
+
|
|
73
|
+
# If current time is within business hours, return now
|
|
74
|
+
if within_business_hours?(now, start_hour, end_hour)
|
|
75
|
+
return now
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# If before business hours today and it's a weekday
|
|
79
|
+
if now.hour < start_hour && !weekend?(now)
|
|
80
|
+
return build_time(now, start_hour, 0, timezone)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Find next business day
|
|
84
|
+
target = next_weekday_at(start_hour, 0, timezone: timezone)
|
|
85
|
+
target
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Returns a specific day of the week at the specified time
|
|
89
|
+
#
|
|
90
|
+
# @param day [Symbol] Day name (:monday, :tuesday, etc.)
|
|
91
|
+
# @param hour [Integer] Hour (0-23)
|
|
92
|
+
# @param minute [Integer] Minute (0-59)
|
|
93
|
+
# @param timezone [String, nil] Timezone name
|
|
94
|
+
# @return [Time]
|
|
95
|
+
def next_day_at(day, hour, minute, timezone: nil)
|
|
96
|
+
days = %i[sunday monday tuesday wednesday thursday friday saturday]
|
|
97
|
+
target_wday = days.index(day.to_sym)
|
|
98
|
+
raise ArgumentError, "Unknown day: #{day}" unless target_wday
|
|
99
|
+
|
|
100
|
+
now = current_time(timezone)
|
|
101
|
+
current_wday = now.wday
|
|
102
|
+
days_ahead = (target_wday - current_wday) % 7
|
|
103
|
+
|
|
104
|
+
# If it's the same day but time has passed, add a week
|
|
105
|
+
if days_ahead == 0
|
|
106
|
+
target = build_time(now, hour, minute, timezone)
|
|
107
|
+
days_ahead = 7 if target <= now
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
future = now + (days_ahead * 86_400)
|
|
111
|
+
build_time(future, hour, minute, timezone)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Returns time at the start of the next month
|
|
115
|
+
#
|
|
116
|
+
# @param day [Integer] Day of month (default: 1)
|
|
117
|
+
# @param hour [Integer] Hour (default: 0)
|
|
118
|
+
# @param minute [Integer] Minute (default: 0)
|
|
119
|
+
# @param timezone [String, nil] Timezone name
|
|
120
|
+
# @return [Time]
|
|
121
|
+
def next_month_at(day: 1, hour: 0, minute: 0, timezone: nil)
|
|
122
|
+
now = current_time(timezone)
|
|
123
|
+
year = now.year
|
|
124
|
+
month = now.month + 1
|
|
125
|
+
|
|
126
|
+
if month > 12
|
|
127
|
+
month = 1
|
|
128
|
+
year += 1
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
Time.new(year, month, day, hour, minute, 0, now.utc_offset)
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# Returns a time offset from now
|
|
135
|
+
#
|
|
136
|
+
# @param seconds [Integer, Float] Seconds to add
|
|
137
|
+
# @param timezone [String, nil] Timezone name
|
|
138
|
+
# @return [Time]
|
|
139
|
+
def from_now(seconds, timezone: nil)
|
|
140
|
+
current_time(timezone) + seconds
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
private
|
|
144
|
+
|
|
145
|
+
def current_time(timezone)
|
|
146
|
+
if timezone && defined?(ActiveSupport::TimeZone)
|
|
147
|
+
ActiveSupport::TimeZone[timezone]&.now || Time.now
|
|
148
|
+
else
|
|
149
|
+
Time.now
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def build_time(base, hour, minute, timezone)
|
|
154
|
+
if timezone && defined?(ActiveSupport::TimeZone)
|
|
155
|
+
zone = ActiveSupport::TimeZone[timezone]
|
|
156
|
+
if zone
|
|
157
|
+
zone.local(base.year, base.month, base.day, hour, minute, 0)
|
|
158
|
+
else
|
|
159
|
+
Time.new(base.year, base.month, base.day, hour, minute, 0, base.utc_offset)
|
|
160
|
+
end
|
|
161
|
+
else
|
|
162
|
+
Time.new(base.year, base.month, base.day, hour, minute, 0, base.utc_offset)
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def weekend?(time)
|
|
167
|
+
time.saturday? || time.sunday?
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def advance_to_next_weekday(time)
|
|
171
|
+
loop do
|
|
172
|
+
time += 86_400 # Add one day
|
|
173
|
+
break unless weekend?(time)
|
|
174
|
+
end
|
|
175
|
+
time
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def within_business_hours?(time, start_hour, end_hour)
|
|
179
|
+
!weekend?(time) &&
|
|
180
|
+
time.hour >= start_hour &&
|
|
181
|
+
time.hour < end_hour
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
end
|
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
module Agents
|
|
5
|
+
class Workflow
|
|
6
|
+
module DSL
|
|
7
|
+
# Configuration object for a workflow step
|
|
8
|
+
#
|
|
9
|
+
# Holds all the configuration options for a step including the agent,
|
|
10
|
+
# input mapping, conditions, retry settings, error handling, and metadata.
|
|
11
|
+
#
|
|
12
|
+
# @example Basic step config
|
|
13
|
+
# StepConfig.new(name: :validate, agent: ValidatorAgent)
|
|
14
|
+
#
|
|
15
|
+
# @example Full configuration
|
|
16
|
+
# StepConfig.new(
|
|
17
|
+
# name: :process,
|
|
18
|
+
# agent: ProcessorAgent,
|
|
19
|
+
# description: "Process the order",
|
|
20
|
+
# timeout: 30,
|
|
21
|
+
# retry_config: { max: 3, on: [Timeout::Error] },
|
|
22
|
+
# critical: true
|
|
23
|
+
# )
|
|
24
|
+
#
|
|
25
|
+
# @api private
|
|
26
|
+
class StepConfig
|
|
27
|
+
attr_reader :name, :agent, :description, :options, :block
|
|
28
|
+
|
|
29
|
+
# @param name [Symbol] Step identifier
|
|
30
|
+
# @param agent [Class, nil] Agent class to execute
|
|
31
|
+
# @param description [String, nil] Human-readable description
|
|
32
|
+
# @param options [Hash] Step configuration options
|
|
33
|
+
# @param block [Proc, nil] Block for routing or custom logic
|
|
34
|
+
def initialize(name:, agent: nil, description: nil, options: {}, block: nil)
|
|
35
|
+
@name = name
|
|
36
|
+
@agent = agent
|
|
37
|
+
@options = normalize_options(options)
|
|
38
|
+
# description can come from direct param or from :desc option
|
|
39
|
+
@description = description || @options[:description]
|
|
40
|
+
@block = block
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Returns whether this step uses routing
|
|
44
|
+
#
|
|
45
|
+
# @return [Boolean]
|
|
46
|
+
def routing?
|
|
47
|
+
options[:on].present? && block.present?
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Returns whether this step has a custom block
|
|
51
|
+
#
|
|
52
|
+
# @return [Boolean]
|
|
53
|
+
def custom_block?
|
|
54
|
+
block.present? && !routing?
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Returns whether this step is optional (continues on failure)
|
|
58
|
+
#
|
|
59
|
+
# @return [Boolean]
|
|
60
|
+
def optional?
|
|
61
|
+
options[:optional] == true
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Returns whether this step executes a sub-workflow
|
|
65
|
+
#
|
|
66
|
+
# @return [Boolean]
|
|
67
|
+
def workflow?
|
|
68
|
+
return false unless agent.present?
|
|
69
|
+
return false unless agent.is_a?(Class)
|
|
70
|
+
|
|
71
|
+
# agent < Workflow returns nil if agent is not a subclass
|
|
72
|
+
(agent < RubyLLM::Agents::Workflow) == true
|
|
73
|
+
rescue TypeError, ArgumentError
|
|
74
|
+
# agent < Workflow raises TypeError/ArgumentError if agent is not a valid Class
|
|
75
|
+
false
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Returns whether this step uses iteration
|
|
79
|
+
#
|
|
80
|
+
# @return [Boolean]
|
|
81
|
+
def iteration?
|
|
82
|
+
options[:each].present?
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Returns the source for iteration items
|
|
86
|
+
#
|
|
87
|
+
# @return [Proc, nil]
|
|
88
|
+
def each_source
|
|
89
|
+
options[:each]
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Returns the concurrency level for iteration
|
|
93
|
+
#
|
|
94
|
+
# @return [Integer, nil]
|
|
95
|
+
def iteration_concurrency
|
|
96
|
+
options[:concurrency]
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Returns whether iteration should fail fast on first error
|
|
100
|
+
#
|
|
101
|
+
# @return [Boolean]
|
|
102
|
+
def iteration_fail_fast?
|
|
103
|
+
options[:fail_fast] == true
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Returns whether iteration should continue on individual item errors
|
|
107
|
+
#
|
|
108
|
+
# @return [Boolean]
|
|
109
|
+
def continue_on_error?
|
|
110
|
+
options[:continue_on_error] == true
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# Returns whether this step is critical (fails workflow on error)
|
|
114
|
+
#
|
|
115
|
+
# @return [Boolean]
|
|
116
|
+
def critical?
|
|
117
|
+
options[:critical] != false && !optional?
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# Returns the timeout for this step
|
|
121
|
+
#
|
|
122
|
+
# @return [Integer, nil] Timeout in seconds
|
|
123
|
+
def timeout
|
|
124
|
+
value = options[:timeout]
|
|
125
|
+
return nil unless value
|
|
126
|
+
value.respond_to?(:to_i) ? value.to_i : value
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# Returns retry configuration
|
|
130
|
+
#
|
|
131
|
+
# @return [Hash] Retry settings
|
|
132
|
+
def retry_config
|
|
133
|
+
@retry_config ||= normalize_retry_config
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# Returns the fallback agent(s)
|
|
137
|
+
#
|
|
138
|
+
# @return [Array<Class>] Fallback agents
|
|
139
|
+
def fallbacks
|
|
140
|
+
@fallbacks ||= Array(options[:fallback]).compact
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# Returns the condition for step execution
|
|
144
|
+
#
|
|
145
|
+
# @return [Symbol, Proc, nil]
|
|
146
|
+
def if_condition
|
|
147
|
+
options[:if]
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# Returns the negative condition for step execution
|
|
151
|
+
#
|
|
152
|
+
# @return [Symbol, Proc, nil]
|
|
153
|
+
def unless_condition
|
|
154
|
+
options[:unless]
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
# Returns the input mapper (lambda or pick config)
|
|
158
|
+
#
|
|
159
|
+
# @return [Proc, nil]
|
|
160
|
+
def input_mapper
|
|
161
|
+
options[:input]
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
# Returns fields to pick from previous step
|
|
165
|
+
#
|
|
166
|
+
# @return [Array<Symbol>, nil]
|
|
167
|
+
def pick_fields
|
|
168
|
+
options[:pick]
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
# Returns the source step for pick operation
|
|
172
|
+
#
|
|
173
|
+
# @return [Symbol, nil]
|
|
174
|
+
def pick_from
|
|
175
|
+
options[:from]
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
# Returns the default value when step is skipped or fails (optional)
|
|
179
|
+
#
|
|
180
|
+
# @return [Object, nil]
|
|
181
|
+
def default_value
|
|
182
|
+
options[:default]
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
# Returns the error handler
|
|
186
|
+
#
|
|
187
|
+
# @return [Symbol, Proc, nil]
|
|
188
|
+
def error_handler
|
|
189
|
+
options[:on_error]
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
# Returns UI-friendly label
|
|
193
|
+
#
|
|
194
|
+
# @return [String, nil]
|
|
195
|
+
def ui_label
|
|
196
|
+
options[:ui_label]
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
# Returns tags for the step
|
|
200
|
+
#
|
|
201
|
+
# @return [Array<Symbol>]
|
|
202
|
+
def tags
|
|
203
|
+
Array(options[:tags])
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
# Returns the throttle duration for this step
|
|
207
|
+
#
|
|
208
|
+
# @return [Integer, Float, nil] Minimum seconds between executions
|
|
209
|
+
def throttle
|
|
210
|
+
value = options[:throttle]
|
|
211
|
+
return nil unless value
|
|
212
|
+
|
|
213
|
+
value.respond_to?(:to_f) ? value.to_f : value
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
# Returns the rate limit configuration for this step
|
|
217
|
+
#
|
|
218
|
+
# @return [Hash, nil] Rate limit config with :calls and :per keys
|
|
219
|
+
def rate_limit
|
|
220
|
+
options[:rate_limit]
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
# Returns whether this step has throttling enabled
|
|
224
|
+
#
|
|
225
|
+
# @return [Boolean]
|
|
226
|
+
def throttled?
|
|
227
|
+
throttle.present? || rate_limit.present?
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
# Resolves the input for this step
|
|
231
|
+
#
|
|
232
|
+
# @param workflow [Workflow] The workflow instance
|
|
233
|
+
# @param previous_result [Result, nil] Previous step result
|
|
234
|
+
# @return [Hash] Input for the agent
|
|
235
|
+
def resolve_input(workflow, previous_result)
|
|
236
|
+
if input_mapper
|
|
237
|
+
workflow.instance_exec(&input_mapper)
|
|
238
|
+
elsif pick_fields
|
|
239
|
+
source = pick_from ? workflow.step_result(pick_from) : previous_result
|
|
240
|
+
source_hash = extract_content_hash(source)
|
|
241
|
+
source_hash.slice(*pick_fields)
|
|
242
|
+
else
|
|
243
|
+
# Default: merge original input with previous step output
|
|
244
|
+
base = workflow.input.to_h
|
|
245
|
+
previous_hash = extract_content_hash(previous_result)
|
|
246
|
+
base.merge(previous_hash)
|
|
247
|
+
end
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
# Resolves the route for routing steps
|
|
251
|
+
#
|
|
252
|
+
# @param workflow [Workflow] The workflow instance
|
|
253
|
+
# @return [Hash] Route configuration with :agent and :options
|
|
254
|
+
def resolve_route(workflow)
|
|
255
|
+
raise "Not a routing step" unless routing?
|
|
256
|
+
|
|
257
|
+
value = workflow.instance_exec(&options[:on])
|
|
258
|
+
builder = RouteBuilder.new
|
|
259
|
+
block.call(builder)
|
|
260
|
+
builder.resolve(value)
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
# Evaluates whether the step should execute
|
|
264
|
+
#
|
|
265
|
+
# @param workflow [Workflow] The workflow instance
|
|
266
|
+
# @return [Boolean]
|
|
267
|
+
def should_execute?(workflow)
|
|
268
|
+
passes_if = if_condition.nil? || evaluate_condition(workflow, if_condition)
|
|
269
|
+
passes_unless = unless_condition.nil? || !evaluate_condition(workflow, unless_condition)
|
|
270
|
+
passes_if && passes_unless
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
# Converts to hash for serialization
|
|
274
|
+
#
|
|
275
|
+
# @return [Hash]
|
|
276
|
+
def to_h
|
|
277
|
+
{
|
|
278
|
+
name: name,
|
|
279
|
+
agent: agent&.name,
|
|
280
|
+
description: description,
|
|
281
|
+
timeout: timeout,
|
|
282
|
+
optional: optional?,
|
|
283
|
+
critical: critical?,
|
|
284
|
+
retry_config: retry_config,
|
|
285
|
+
fallbacks: fallbacks.map(&:name),
|
|
286
|
+
tags: tags,
|
|
287
|
+
ui_label: ui_label,
|
|
288
|
+
workflow: workflow?,
|
|
289
|
+
iteration: iteration?,
|
|
290
|
+
iteration_concurrency: iteration_concurrency,
|
|
291
|
+
iteration_fail_fast: iteration_fail_fast?,
|
|
292
|
+
throttle: throttle,
|
|
293
|
+
rate_limit: rate_limit
|
|
294
|
+
}.compact
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
private
|
|
298
|
+
|
|
299
|
+
def normalize_options(opts)
|
|
300
|
+
# Handle desc as alias for description
|
|
301
|
+
opts[:description] ||= opts.delete(:desc)
|
|
302
|
+
opts
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
def normalize_retry_config
|
|
306
|
+
retry_opt = options[:retry]
|
|
307
|
+
on_opt = options[:on]
|
|
308
|
+
|
|
309
|
+
case retry_opt
|
|
310
|
+
when Integer
|
|
311
|
+
{
|
|
312
|
+
max: retry_opt,
|
|
313
|
+
on: normalize_error_classes(on_opt) || [StandardError],
|
|
314
|
+
backoff: :none,
|
|
315
|
+
delay: 1
|
|
316
|
+
}
|
|
317
|
+
when Hash
|
|
318
|
+
{
|
|
319
|
+
max: retry_opt[:max] || 3,
|
|
320
|
+
on: normalize_error_classes(retry_opt[:on] || on_opt) || [StandardError],
|
|
321
|
+
backoff: retry_opt[:backoff] || :none,
|
|
322
|
+
delay: retry_opt[:delay] || 1
|
|
323
|
+
}
|
|
324
|
+
else
|
|
325
|
+
{ max: 0, on: [], backoff: :none, delay: 1 }
|
|
326
|
+
end
|
|
327
|
+
end
|
|
328
|
+
|
|
329
|
+
def normalize_error_classes(classes)
|
|
330
|
+
return nil if classes.nil?
|
|
331
|
+
Array(classes)
|
|
332
|
+
end
|
|
333
|
+
|
|
334
|
+
def evaluate_condition(workflow, condition)
|
|
335
|
+
case condition
|
|
336
|
+
when Symbol then workflow.send(condition)
|
|
337
|
+
when Proc then workflow.instance_exec(&condition)
|
|
338
|
+
else condition
|
|
339
|
+
end
|
|
340
|
+
end
|
|
341
|
+
|
|
342
|
+
def extract_content_hash(result)
|
|
343
|
+
return {} if result.nil?
|
|
344
|
+
|
|
345
|
+
content = result.respond_to?(:content) ? result.content : result
|
|
346
|
+
content.is_a?(Hash) ? content : {}
|
|
347
|
+
end
|
|
348
|
+
end
|
|
349
|
+
end
|
|
350
|
+
end
|
|
351
|
+
end
|
|
352
|
+
end
|