ruby_llm-agents 1.3.4 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (191) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +112 -336
  3. data/app/controllers/concerns/ruby_llm/agents/sortable.rb +0 -1
  4. data/app/controllers/ruby_llm/agents/agents_controller.rb +5 -56
  5. data/app/controllers/ruby_llm/agents/dashboard_controller.rb +22 -106
  6. data/app/controllers/ruby_llm/agents/executions_controller.rb +4 -114
  7. data/app/controllers/ruby_llm/agents/tenants_controller.rb +30 -2
  8. data/app/helpers/ruby_llm/agents/application_helper.rb +19 -53
  9. data/app/models/ruby_llm/agents/execution/analytics.rb +13 -54
  10. data/app/models/ruby_llm/agents/execution/scopes.rb +61 -14
  11. data/app/models/ruby_llm/agents/execution.rb +52 -12
  12. data/app/models/ruby_llm/agents/execution_detail.rb +18 -0
  13. data/app/models/ruby_llm/agents/tenant/budgetable.rb +132 -24
  14. data/app/models/ruby_llm/agents/tenant/incrementable.rb +117 -0
  15. data/app/models/ruby_llm/agents/tenant/resettable.rb +128 -0
  16. data/app/models/ruby_llm/agents/tenant/trackable.rb +46 -12
  17. data/app/models/ruby_llm/agents/tenant.rb +2 -3
  18. data/app/models/ruby_llm/agents/tenant_budget.rb +6 -3
  19. data/app/services/ruby_llm/agents/agent_registry.rb +6 -112
  20. data/app/views/layouts/ruby_llm/agents/application.html.erb +89 -252
  21. data/app/views/ruby_llm/agents/agents/_config_agent.html.erb +71 -218
  22. data/app/views/ruby_llm/agents/agents/_config_embedder.html.erb +20 -63
  23. data/app/views/ruby_llm/agents/agents/_config_image_generator.html.erb +44 -131
  24. data/app/views/ruby_llm/agents/agents/_config_moderator.html.erb +16 -57
  25. data/app/views/ruby_llm/agents/agents/_config_speaker.html.erb +39 -104
  26. data/app/views/ruby_llm/agents/agents/_config_transcriber.html.erb +29 -82
  27. data/app/views/ruby_llm/agents/agents/_empty_state.html.erb +4 -14
  28. data/app/views/ruby_llm/agents/agents/index.html.erb +105 -274
  29. data/app/views/ruby_llm/agents/agents/show.html.erb +248 -378
  30. data/app/views/ruby_llm/agents/dashboard/_action_center.html.erb +29 -52
  31. data/app/views/ruby_llm/agents/dashboard/_tenant_budget.html.erb +73 -99
  32. data/app/views/ruby_llm/agents/dashboard/index.html.erb +228 -433
  33. data/app/views/ruby_llm/agents/executions/_execution.html.erb +1 -1
  34. data/app/views/ruby_llm/agents/executions/_filters.html.erb +4 -25
  35. data/app/views/ruby_llm/agents/executions/_list.html.erb +111 -152
  36. data/app/views/ruby_llm/agents/executions/index.html.erb +5 -7
  37. data/app/views/ruby_llm/agents/executions/show.html.erb +526 -1037
  38. data/app/views/ruby_llm/agents/shared/_agent_type_badge.html.erb +5 -21
  39. data/app/views/ruby_llm/agents/shared/_executions_table.html.erb +70 -191
  40. data/app/views/ruby_llm/agents/shared/_filter_dropdown.html.erb +16 -44
  41. data/app/views/ruby_llm/agents/shared/_select_dropdown.html.erb +12 -41
  42. data/app/views/ruby_llm/agents/shared/_status_badge.html.erb +11 -65
  43. data/app/views/ruby_llm/agents/shared/_tenant_filter.html.erb +6 -5
  44. data/app/views/ruby_llm/agents/system_config/show.html.erb +240 -351
  45. data/app/views/ruby_llm/agents/tenants/_form.html.erb +67 -77
  46. data/app/views/ruby_llm/agents/tenants/edit.html.erb +7 -9
  47. data/app/views/ruby_llm/agents/tenants/index.html.erb +100 -122
  48. data/app/views/ruby_llm/agents/tenants/show.html.erb +146 -336
  49. data/config/routes.rb +0 -13
  50. data/lib/generators/ruby_llm_agents/install_generator.rb +13 -17
  51. data/lib/generators/ruby_llm_agents/migrate_structure_generator.rb +2 -12
  52. data/lib/generators/ruby_llm_agents/restructure_generator.rb +0 -2
  53. data/lib/generators/ruby_llm_agents/templates/add_usage_counters_to_tenants_migration.rb.tt +37 -0
  54. data/lib/generators/ruby_llm_agents/templates/agent.rb.tt +1 -2
  55. data/lib/generators/ruby_llm_agents/templates/application_agent.rb.tt +1 -1
  56. data/lib/generators/ruby_llm_agents/templates/application_image_pipeline.rb.tt +0 -1
  57. data/lib/generators/ruby_llm_agents/templates/create_execution_details_migration.rb.tt +27 -0
  58. data/lib/generators/ruby_llm_agents/templates/create_tenants_migration.rb.tt +25 -0
  59. data/lib/generators/ruby_llm_agents/templates/image_pipeline.rb.tt +0 -1
  60. data/lib/generators/ruby_llm_agents/templates/initializer.rb.tt +33 -12
  61. data/lib/generators/ruby_llm_agents/templates/migration.rb.tt +40 -71
  62. data/lib/generators/ruby_llm_agents/templates/remove_agent_version_migration.rb.tt +13 -0
  63. data/lib/generators/ruby_llm_agents/templates/remove_workflow_columns_migration.rb.tt +19 -0
  64. data/lib/generators/ruby_llm_agents/templates/skills/AGENTS.md.tt +2 -4
  65. data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_PIPELINES.md.tt +0 -1
  66. data/lib/generators/ruby_llm_agents/templates/split_execution_details_migration.rb.tt +232 -0
  67. data/lib/generators/ruby_llm_agents/upgrade_generator.rb +77 -259
  68. data/lib/ruby_llm/agents/audio/speaker.rb +0 -1
  69. data/lib/ruby_llm/agents/audio/transcriber.rb +0 -1
  70. data/lib/ruby_llm/agents/base_agent.rb +54 -23
  71. data/lib/ruby_llm/agents/core/base/callbacks.rb +142 -0
  72. data/lib/ruby_llm/agents/core/base.rb +23 -55
  73. data/lib/ruby_llm/agents/core/configuration.rb +97 -117
  74. data/lib/ruby_llm/agents/core/errors.rb +0 -58
  75. data/lib/ruby_llm/agents/core/instrumentation.rb +157 -110
  76. data/lib/ruby_llm/agents/core/llm_tenant.rb +8 -7
  77. data/lib/ruby_llm/agents/core/version.rb +1 -1
  78. data/lib/ruby_llm/agents/dsl/base.rb +157 -17
  79. data/lib/ruby_llm/agents/dsl/caching.rb +33 -2
  80. data/lib/ruby_llm/agents/dsl/reliability.rb +148 -0
  81. data/lib/ruby_llm/agents/dsl.rb +1 -2
  82. data/lib/ruby_llm/agents/image/analyzer/execution.rb +1 -2
  83. data/lib/ruby_llm/agents/image/background_remover/execution.rb +1 -2
  84. data/lib/ruby_llm/agents/image/concerns/image_operation_dsl.rb +1 -13
  85. data/lib/ruby_llm/agents/image/concerns/image_operation_execution.rb +2 -2
  86. data/lib/ruby_llm/agents/image/editor/dsl.rb +0 -14
  87. data/lib/ruby_llm/agents/image/editor/execution.rb +1 -10
  88. data/lib/ruby_llm/agents/image/editor.rb +0 -1
  89. data/lib/ruby_llm/agents/image/generator.rb +0 -21
  90. data/lib/ruby_llm/agents/image/pipeline/dsl.rb +0 -13
  91. data/lib/ruby_llm/agents/image/pipeline/execution.rb +0 -1
  92. data/lib/ruby_llm/agents/image/transformer/dsl.rb +0 -13
  93. data/lib/ruby_llm/agents/image/transformer/execution.rb +1 -10
  94. data/lib/ruby_llm/agents/image/transformer.rb +0 -1
  95. data/lib/ruby_llm/agents/image/upscaler/execution.rb +1 -2
  96. data/lib/ruby_llm/agents/image/variator/execution.rb +1 -2
  97. data/lib/ruby_llm/agents/infrastructure/alert_manager.rb +78 -173
  98. data/lib/ruby_llm/agents/infrastructure/budget/budget_query.rb +66 -2
  99. data/lib/ruby_llm/agents/infrastructure/budget/spend_recorder.rb +0 -12
  100. data/lib/ruby_llm/agents/infrastructure/circuit_breaker.rb +10 -13
  101. data/lib/ruby_llm/agents/infrastructure/execution_logger_job.rb +8 -0
  102. data/lib/ruby_llm/agents/pipeline/context.rb +0 -1
  103. data/lib/ruby_llm/agents/pipeline/middleware/budget.rb +28 -4
  104. data/lib/ruby_llm/agents/pipeline/middleware/cache.rb +3 -10
  105. data/lib/ruby_llm/agents/pipeline/middleware/instrumentation.rb +88 -55
  106. data/lib/ruby_llm/agents/pipeline/middleware/tenant.rb +5 -41
  107. data/lib/ruby_llm/agents/rails/engine.rb +6 -6
  108. data/lib/ruby_llm/agents/results/base.rb +1 -49
  109. data/lib/ruby_llm/agents/text/embedder.rb +0 -1
  110. data/lib/ruby_llm/agents.rb +1 -9
  111. data/lib/tasks/ruby_llm_agents.rake +34 -0
  112. metadata +14 -83
  113. data/app/controllers/ruby_llm/agents/api_configurations_controller.rb +0 -214
  114. data/app/controllers/ruby_llm/agents/workflows_controller.rb +0 -544
  115. data/app/mailers/ruby_llm/agents/alert_mailer.rb +0 -84
  116. data/app/mailers/ruby_llm/agents/application_mailer.rb +0 -28
  117. data/app/models/ruby_llm/agents/api_configuration.rb +0 -386
  118. data/app/models/ruby_llm/agents/execution/workflow.rb +0 -170
  119. data/app/models/ruby_llm/agents/tenant/configurable.rb +0 -135
  120. data/app/views/ruby_llm/agents/agents/_agent.html.erb +0 -98
  121. data/app/views/ruby_llm/agents/agents/_version_comparison.html.erb +0 -186
  122. data/app/views/ruby_llm/agents/agents/_workflow.html.erb +0 -126
  123. data/app/views/ruby_llm/agents/alert_mailer/alert_notification.html.erb +0 -107
  124. data/app/views/ruby_llm/agents/alert_mailer/alert_notification.text.erb +0 -18
  125. data/app/views/ruby_llm/agents/api_configurations/_api_key_field.html.erb +0 -34
  126. data/app/views/ruby_llm/agents/api_configurations/_form.html.erb +0 -288
  127. data/app/views/ruby_llm/agents/api_configurations/edit.html.erb +0 -95
  128. data/app/views/ruby_llm/agents/api_configurations/edit_tenant.html.erb +0 -97
  129. data/app/views/ruby_llm/agents/api_configurations/show.html.erb +0 -214
  130. data/app/views/ruby_llm/agents/api_configurations/tenant.html.erb +0 -179
  131. data/app/views/ruby_llm/agents/dashboard/_agent_comparison.html.erb +0 -73
  132. data/app/views/ruby_llm/agents/dashboard/_alerts_feed.html.erb +0 -62
  133. data/app/views/ruby_llm/agents/dashboard/_breaker_strip.html.erb +0 -47
  134. data/app/views/ruby_llm/agents/dashboard/_budgets_bar.html.erb +0 -75
  135. data/app/views/ruby_llm/agents/dashboard/_model_comparison.html.erb +0 -56
  136. data/app/views/ruby_llm/agents/dashboard/_model_cost_breakdown.html.erb +0 -115
  137. data/app/views/ruby_llm/agents/dashboard/_now_strip.html.erb +0 -59
  138. data/app/views/ruby_llm/agents/dashboard/_top_errors.html.erb +0 -60
  139. data/app/views/ruby_llm/agents/executions/_workflow_summary.html.erb +0 -86
  140. data/app/views/ruby_llm/agents/executions/dry_run.html.erb +0 -149
  141. data/app/views/ruby_llm/agents/shared/_breadcrumbs.html.erb +0 -48
  142. data/app/views/ruby_llm/agents/shared/_nav_link.html.erb +0 -27
  143. data/app/views/ruby_llm/agents/shared/_stat_card.html.erb +0 -14
  144. data/app/views/ruby_llm/agents/shared/_workflow_type_badge.html.erb +0 -35
  145. data/app/views/ruby_llm/agents/workflows/_empty_state.html.erb +0 -22
  146. data/app/views/ruby_llm/agents/workflows/_step_performance.html.erb +0 -228
  147. data/app/views/ruby_llm/agents/workflows/_structure_dsl.html.erb +0 -539
  148. data/app/views/ruby_llm/agents/workflows/_structure_parallel.html.erb +0 -76
  149. data/app/views/ruby_llm/agents/workflows/_structure_pipeline.html.erb +0 -74
  150. data/app/views/ruby_llm/agents/workflows/_structure_router.html.erb +0 -108
  151. data/app/views/ruby_llm/agents/workflows/_workflow_diagram.html.erb +0 -920
  152. data/app/views/ruby_llm/agents/workflows/index.html.erb +0 -179
  153. data/app/views/ruby_llm/agents/workflows/show.html.erb +0 -467
  154. data/lib/generators/ruby_llm_agents/api_configuration_generator.rb +0 -100
  155. data/lib/generators/ruby_llm_agents/templates/add_workflow_migration.rb.tt +0 -38
  156. data/lib/generators/ruby_llm_agents/templates/application_workflow.rb.tt +0 -48
  157. data/lib/generators/ruby_llm_agents/templates/create_api_configurations_migration.rb.tt +0 -90
  158. data/lib/generators/ruby_llm_agents/templates/skills/WORKFLOWS.md.tt +0 -551
  159. data/lib/ruby_llm/agents/core/base/moderation_dsl.rb +0 -181
  160. data/lib/ruby_llm/agents/core/base/moderation_execution.rb +0 -274
  161. data/lib/ruby_llm/agents/core/resolved_config.rb +0 -348
  162. data/lib/ruby_llm/agents/image/generator/content_policy.rb +0 -95
  163. data/lib/ruby_llm/agents/infrastructure/redactor.rb +0 -130
  164. data/lib/ruby_llm/agents/results/moderation_result.rb +0 -158
  165. data/lib/ruby_llm/agents/text/moderator.rb +0 -237
  166. data/lib/ruby_llm/agents/workflow/approval.rb +0 -205
  167. data/lib/ruby_llm/agents/workflow/approval_store.rb +0 -179
  168. data/lib/ruby_llm/agents/workflow/async.rb +0 -220
  169. data/lib/ruby_llm/agents/workflow/async_executor.rb +0 -156
  170. data/lib/ruby_llm/agents/workflow/dsl/executor.rb +0 -467
  171. data/lib/ruby_llm/agents/workflow/dsl/input_schema.rb +0 -244
  172. data/lib/ruby_llm/agents/workflow/dsl/iteration_executor.rb +0 -289
  173. data/lib/ruby_llm/agents/workflow/dsl/parallel_group.rb +0 -107
  174. data/lib/ruby_llm/agents/workflow/dsl/route_builder.rb +0 -150
  175. data/lib/ruby_llm/agents/workflow/dsl/schedule_helpers.rb +0 -187
  176. data/lib/ruby_llm/agents/workflow/dsl/step_config.rb +0 -352
  177. data/lib/ruby_llm/agents/workflow/dsl/step_executor.rb +0 -415
  178. data/lib/ruby_llm/agents/workflow/dsl/wait_config.rb +0 -257
  179. data/lib/ruby_llm/agents/workflow/dsl/wait_executor.rb +0 -317
  180. data/lib/ruby_llm/agents/workflow/dsl.rb +0 -576
  181. data/lib/ruby_llm/agents/workflow/instrumentation.rb +0 -249
  182. data/lib/ruby_llm/agents/workflow/notifiers/base.rb +0 -117
  183. data/lib/ruby_llm/agents/workflow/notifiers/email.rb +0 -117
  184. data/lib/ruby_llm/agents/workflow/notifiers/slack.rb +0 -180
  185. data/lib/ruby_llm/agents/workflow/notifiers/webhook.rb +0 -121
  186. data/lib/ruby_llm/agents/workflow/notifiers.rb +0 -70
  187. data/lib/ruby_llm/agents/workflow/orchestrator.rb +0 -416
  188. data/lib/ruby_llm/agents/workflow/result.rb +0 -592
  189. data/lib/ruby_llm/agents/workflow/thread_pool.rb +0 -185
  190. data/lib/ruby_llm/agents/workflow/throttle_manager.rb +0 -206
  191. data/lib/ruby_llm/agents/workflow/wait_result.rb +0 -213
@@ -1,150 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module RubyLLM
4
- module Agents
5
- class Workflow
6
- module DSL
7
- # Builder for defining routing options in a step
8
- #
9
- # Used with the `on:` option to route to different agents based on
10
- # a runtime value. Supports a fluent interface for defining routes.
11
- #
12
- # @example Basic routing
13
- # step :process, on: -> { enrich.tier } do |route|
14
- # route.premium PremiumAgent
15
- # route.standard StandardAgent
16
- # route.default DefaultAgent
17
- # end
18
- #
19
- # @example With per-route options
20
- # step :process, on: -> { enrich.tier } do |route|
21
- # route.premium PremiumAgent, input: -> { { vip: true } }, timeout: 5.minutes
22
- # route.standard StandardAgent
23
- # route.default DefaultAgent
24
- # end
25
- #
26
- # @api private
27
- class RouteBuilder
28
- # Error raised when no route matches and no default is defined
29
- class NoRouteError < StandardError
30
- attr_reader :value, :available_routes
31
-
32
- def initialize(message, value: nil, available_routes: [])
33
- super(message)
34
- @value = value
35
- @available_routes = available_routes
36
- end
37
- end
38
-
39
- def initialize
40
- @routes = {}
41
- @default = nil
42
- end
43
-
44
- # Returns all defined routes
45
- #
46
- # @return [Hash<Symbol, Hash>]
47
- attr_reader :routes
48
-
49
- # Returns or sets the default route
50
- #
51
- # When called with no arguments, returns the current default.
52
- # When called with an agent, sets the default route.
53
- #
54
- # @param agent [Class, nil] Agent class for the default route
55
- # @param options [Hash] Route options
56
- # @return [Hash, nil]
57
- def default(agent = nil, **options)
58
- if agent.nil? && options.empty?
59
- @default
60
- else
61
- @default = { agent: agent, options: options }
62
- end
63
- end
64
-
65
- # Handles dynamic route definitions
66
- #
67
- # Any method call becomes a route definition.
68
- #
69
- # @param name [Symbol] Route name
70
- # @param agent [Class] Agent class for this route
71
- # @param options [Hash] Route options
72
- # @return [void]
73
- def method_missing(name, agent = nil, **options)
74
- if name == :default
75
- @default = { agent: agent, options: options }
76
- else
77
- @routes[name.to_sym] = { agent: agent, options: options }
78
- end
79
- end
80
-
81
- def respond_to_missing?(name, include_private = false)
82
- true
83
- end
84
-
85
- # Resolves the route for a given value
86
- #
87
- # @param value [Object] The routing key value
88
- # @return [Hash] Route configuration with :agent and :options
89
- # @raise [NoRouteError] If no route matches and no default is defined
90
- def resolve(value)
91
- key = normalize_key(value)
92
-
93
- route = @routes[key] || @default
94
-
95
- unless route
96
- raise NoRouteError.new(
97
- "No route defined for value: #{value.inspect} (normalized: #{key}). " \
98
- "Available routes: #{@routes.keys.join(', ')}",
99
- value: value,
100
- available_routes: @routes.keys
101
- )
102
- end
103
-
104
- route
105
- end
106
-
107
- # Returns all route names
108
- #
109
- # @return [Array<Symbol>]
110
- def route_names
111
- @routes.keys
112
- end
113
-
114
- # Checks if a route exists
115
- #
116
- # @param name [Symbol] Route name
117
- # @return [Boolean]
118
- def route_exists?(name)
119
- @routes.key?(name.to_sym) || @default.present?
120
- end
121
-
122
- # Converts to hash for serialization
123
- #
124
- # @return [Hash]
125
- def to_h
126
- {
127
- routes: @routes.transform_values do |r|
128
- { agent: r[:agent]&.name, options: r[:options] }
129
- end,
130
- default: @default ? { agent: @default[:agent]&.name, options: @default[:options] } : nil
131
- }
132
- end
133
-
134
- private
135
-
136
- def normalize_key(value)
137
- case value
138
- when Symbol then value
139
- when String then value.to_sym
140
- when TrueClass then :true
141
- when FalseClass then :false
142
- when NilClass then :nil
143
- else value.to_s.to_sym
144
- end
145
- end
146
- end
147
- end
148
- end
149
- end
150
- end
@@ -1,187 +0,0 @@
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
@@ -1,352 +0,0 @@
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