ruby_llm-agents 1.3.4 → 2.0.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 (190) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +101 -334
  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 +46 -10
  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 +87 -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 +9 -14
  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 +9 -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 +58 -262
  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 +52 -6
  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 +58 -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/pipeline/context.rb +0 -1
  102. data/lib/ruby_llm/agents/pipeline/middleware/budget.rb +28 -4
  103. data/lib/ruby_llm/agents/pipeline/middleware/cache.rb +3 -10
  104. data/lib/ruby_llm/agents/pipeline/middleware/instrumentation.rb +88 -55
  105. data/lib/ruby_llm/agents/pipeline/middleware/tenant.rb +5 -41
  106. data/lib/ruby_llm/agents/rails/engine.rb +6 -6
  107. data/lib/ruby_llm/agents/results/base.rb +1 -49
  108. data/lib/ruby_llm/agents/text/embedder.rb +0 -1
  109. data/lib/ruby_llm/agents.rb +1 -9
  110. data/lib/tasks/ruby_llm_agents.rake +34 -0
  111. metadata +12 -81
  112. data/app/controllers/ruby_llm/agents/api_configurations_controller.rb +0 -214
  113. data/app/controllers/ruby_llm/agents/workflows_controller.rb +0 -544
  114. data/app/mailers/ruby_llm/agents/alert_mailer.rb +0 -84
  115. data/app/mailers/ruby_llm/agents/application_mailer.rb +0 -28
  116. data/app/models/ruby_llm/agents/api_configuration.rb +0 -386
  117. data/app/models/ruby_llm/agents/execution/workflow.rb +0 -170
  118. data/app/models/ruby_llm/agents/tenant/configurable.rb +0 -135
  119. data/app/views/ruby_llm/agents/agents/_agent.html.erb +0 -98
  120. data/app/views/ruby_llm/agents/agents/_version_comparison.html.erb +0 -186
  121. data/app/views/ruby_llm/agents/agents/_workflow.html.erb +0 -126
  122. data/app/views/ruby_llm/agents/alert_mailer/alert_notification.html.erb +0 -107
  123. data/app/views/ruby_llm/agents/alert_mailer/alert_notification.text.erb +0 -18
  124. data/app/views/ruby_llm/agents/api_configurations/_api_key_field.html.erb +0 -34
  125. data/app/views/ruby_llm/agents/api_configurations/_form.html.erb +0 -288
  126. data/app/views/ruby_llm/agents/api_configurations/edit.html.erb +0 -95
  127. data/app/views/ruby_llm/agents/api_configurations/edit_tenant.html.erb +0 -97
  128. data/app/views/ruby_llm/agents/api_configurations/show.html.erb +0 -214
  129. data/app/views/ruby_llm/agents/api_configurations/tenant.html.erb +0 -179
  130. data/app/views/ruby_llm/agents/dashboard/_agent_comparison.html.erb +0 -73
  131. data/app/views/ruby_llm/agents/dashboard/_alerts_feed.html.erb +0 -62
  132. data/app/views/ruby_llm/agents/dashboard/_breaker_strip.html.erb +0 -47
  133. data/app/views/ruby_llm/agents/dashboard/_budgets_bar.html.erb +0 -75
  134. data/app/views/ruby_llm/agents/dashboard/_model_comparison.html.erb +0 -56
  135. data/app/views/ruby_llm/agents/dashboard/_model_cost_breakdown.html.erb +0 -115
  136. data/app/views/ruby_llm/agents/dashboard/_now_strip.html.erb +0 -59
  137. data/app/views/ruby_llm/agents/dashboard/_top_errors.html.erb +0 -60
  138. data/app/views/ruby_llm/agents/executions/_workflow_summary.html.erb +0 -86
  139. data/app/views/ruby_llm/agents/executions/dry_run.html.erb +0 -149
  140. data/app/views/ruby_llm/agents/shared/_breadcrumbs.html.erb +0 -48
  141. data/app/views/ruby_llm/agents/shared/_nav_link.html.erb +0 -27
  142. data/app/views/ruby_llm/agents/shared/_stat_card.html.erb +0 -14
  143. data/app/views/ruby_llm/agents/shared/_workflow_type_badge.html.erb +0 -35
  144. data/app/views/ruby_llm/agents/workflows/_empty_state.html.erb +0 -22
  145. data/app/views/ruby_llm/agents/workflows/_step_performance.html.erb +0 -228
  146. data/app/views/ruby_llm/agents/workflows/_structure_dsl.html.erb +0 -539
  147. data/app/views/ruby_llm/agents/workflows/_structure_parallel.html.erb +0 -76
  148. data/app/views/ruby_llm/agents/workflows/_structure_pipeline.html.erb +0 -74
  149. data/app/views/ruby_llm/agents/workflows/_structure_router.html.erb +0 -108
  150. data/app/views/ruby_llm/agents/workflows/_workflow_diagram.html.erb +0 -920
  151. data/app/views/ruby_llm/agents/workflows/index.html.erb +0 -179
  152. data/app/views/ruby_llm/agents/workflows/show.html.erb +0 -467
  153. data/lib/generators/ruby_llm_agents/api_configuration_generator.rb +0 -100
  154. data/lib/generators/ruby_llm_agents/templates/add_workflow_migration.rb.tt +0 -38
  155. data/lib/generators/ruby_llm_agents/templates/application_workflow.rb.tt +0 -48
  156. data/lib/generators/ruby_llm_agents/templates/create_api_configurations_migration.rb.tt +0 -90
  157. data/lib/generators/ruby_llm_agents/templates/skills/WORKFLOWS.md.tt +0 -551
  158. data/lib/ruby_llm/agents/core/base/moderation_dsl.rb +0 -181
  159. data/lib/ruby_llm/agents/core/base/moderation_execution.rb +0 -274
  160. data/lib/ruby_llm/agents/core/resolved_config.rb +0 -348
  161. data/lib/ruby_llm/agents/image/generator/content_policy.rb +0 -95
  162. data/lib/ruby_llm/agents/infrastructure/redactor.rb +0 -130
  163. data/lib/ruby_llm/agents/results/moderation_result.rb +0 -158
  164. data/lib/ruby_llm/agents/text/moderator.rb +0 -237
  165. data/lib/ruby_llm/agents/workflow/approval.rb +0 -205
  166. data/lib/ruby_llm/agents/workflow/approval_store.rb +0 -179
  167. data/lib/ruby_llm/agents/workflow/async.rb +0 -220
  168. data/lib/ruby_llm/agents/workflow/async_executor.rb +0 -156
  169. data/lib/ruby_llm/agents/workflow/dsl/executor.rb +0 -467
  170. data/lib/ruby_llm/agents/workflow/dsl/input_schema.rb +0 -244
  171. data/lib/ruby_llm/agents/workflow/dsl/iteration_executor.rb +0 -289
  172. data/lib/ruby_llm/agents/workflow/dsl/parallel_group.rb +0 -107
  173. data/lib/ruby_llm/agents/workflow/dsl/route_builder.rb +0 -150
  174. data/lib/ruby_llm/agents/workflow/dsl/schedule_helpers.rb +0 -187
  175. data/lib/ruby_llm/agents/workflow/dsl/step_config.rb +0 -352
  176. data/lib/ruby_llm/agents/workflow/dsl/step_executor.rb +0 -415
  177. data/lib/ruby_llm/agents/workflow/dsl/wait_config.rb +0 -257
  178. data/lib/ruby_llm/agents/workflow/dsl/wait_executor.rb +0 -317
  179. data/lib/ruby_llm/agents/workflow/dsl.rb +0 -576
  180. data/lib/ruby_llm/agents/workflow/instrumentation.rb +0 -249
  181. data/lib/ruby_llm/agents/workflow/notifiers/base.rb +0 -117
  182. data/lib/ruby_llm/agents/workflow/notifiers/email.rb +0 -117
  183. data/lib/ruby_llm/agents/workflow/notifiers/slack.rb +0 -180
  184. data/lib/ruby_llm/agents/workflow/notifiers/webhook.rb +0 -121
  185. data/lib/ruby_llm/agents/workflow/notifiers.rb +0 -70
  186. data/lib/ruby_llm/agents/workflow/orchestrator.rb +0 -416
  187. data/lib/ruby_llm/agents/workflow/result.rb +0 -592
  188. data/lib/ruby_llm/agents/workflow/thread_pool.rb +0 -185
  189. data/lib/ruby_llm/agents/workflow/throttle_manager.rb +0 -206
  190. data/lib/ruby_llm/agents/workflow/wait_result.rb +0 -213
@@ -1,121 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "net/http"
4
- require "uri"
5
- require "json"
6
-
7
- module RubyLLM
8
- module Agents
9
- class Workflow
10
- module Notifiers
11
- # Generic webhook notification adapter for approval requests
12
- #
13
- # Posts approval notifications to any HTTP endpoint.
14
- # Supports custom headers for authentication and content negotiation.
15
- #
16
- # @example Basic usage
17
- # notifier = Webhook.new(url: "https://api.example.com/approvals")
18
- # notifier.notify(approval, "Please review")
19
- #
20
- # @example With authentication
21
- # notifier = Webhook.new(
22
- # url: "https://api.example.com/approvals",
23
- # headers: { "Authorization" => "Bearer token123" }
24
- # )
25
- #
26
- # @api public
27
- class Webhook < Base
28
- class << self
29
- attr_accessor :default_url, :default_headers, :timeout
30
-
31
- # Configure the webhook notifier
32
- #
33
- # @yield [self] The webhook notifier class
34
- # @return [void]
35
- def configure
36
- yield self
37
- end
38
-
39
- # Reset configuration to defaults
40
- #
41
- # @return [void]
42
- def reset!
43
- @default_url = nil
44
- @default_headers = nil
45
- @timeout = nil
46
- end
47
- end
48
-
49
- # @param url [String] The webhook URL
50
- # @param headers [Hash] Additional HTTP headers
51
- # @param timeout [Integer] Request timeout in seconds
52
- def initialize(url: nil, headers: {}, timeout: nil)
53
- @url = url || self.class.default_url
54
- @headers = (self.class.default_headers || {}).merge(headers)
55
- @timeout = timeout || self.class.timeout || 10
56
- end
57
-
58
- # Send a webhook notification
59
- #
60
- # @param approval [Approval] The approval request
61
- # @param message [String] The notification message
62
- # @return [Boolean] true if webhook returned 2xx status
63
- def notify(approval, message)
64
- return false unless @url
65
-
66
- payload = build_payload(approval, message)
67
- send_request(payload)
68
- rescue StandardError => e
69
- handle_error(e, approval)
70
- false
71
- end
72
-
73
- private
74
-
75
- def build_payload(approval, message)
76
- {
77
- event: "approval_requested",
78
- approval: {
79
- id: approval.id,
80
- workflow_id: approval.workflow_id,
81
- workflow_type: approval.workflow_type,
82
- name: approval.name,
83
- status: approval.status,
84
- approvers: approval.approvers,
85
- expires_at: approval.expires_at&.iso8601,
86
- created_at: approval.created_at.iso8601,
87
- metadata: approval.metadata
88
- },
89
- message: message,
90
- timestamp: Time.now.iso8601
91
- }
92
- end
93
-
94
- def send_request(payload)
95
- uri = URI.parse(@url)
96
- http = Net::HTTP.new(uri.host, uri.port)
97
- http.use_ssl = uri.scheme == "https"
98
- http.open_timeout = @timeout
99
- http.read_timeout = @timeout
100
-
101
- request = Net::HTTP::Post.new(uri.request_uri)
102
- request["Content-Type"] = "application/json"
103
- @headers.each { |key, value| request[key] = value }
104
- request.body = payload.to_json
105
-
106
- response = http.request(request)
107
- response.code.to_i.between?(200, 299)
108
- end
109
-
110
- def handle_error(error, approval)
111
- if defined?(Rails) && Rails.logger
112
- Rails.logger.error(
113
- "[RubyLLM::Agents] Webhook notification failed for approval #{approval.id}: #{error.message}"
114
- )
115
- end
116
- end
117
- end
118
- end
119
- end
120
- end
121
- end
@@ -1,70 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative "notifiers/base"
4
- require_relative "notifiers/email"
5
- require_relative "notifiers/slack"
6
- require_relative "notifiers/webhook"
7
-
8
- module RubyLLM
9
- module Agents
10
- class Workflow
11
- module Notifiers
12
- # Configure and register default notifiers
13
- #
14
- # @example Register notifiers
15
- # RubyLLM::Agents::Workflow::Notifiers.setup do |config|
16
- # config.register :email, Email.new
17
- # config.register :slack, Slack.new(webhook_url: "...")
18
- # end
19
- #
20
- # @api public
21
- class << self
22
- # Setup notifiers with configuration
23
- #
24
- # @yield [Registry] The notifier registry
25
- # @return [void]
26
- def setup
27
- yield Registry
28
- end
29
-
30
- # Register a notifier
31
- #
32
- # @param name [Symbol] The notifier name
33
- # @param notifier [Base] The notifier instance
34
- # @return [void]
35
- def register(name, notifier)
36
- Registry.register(name, notifier)
37
- end
38
-
39
- # Get a notifier
40
- #
41
- # @param name [Symbol] The notifier name
42
- # @return [Base, nil]
43
- def [](name)
44
- Registry.get(name)
45
- end
46
-
47
- # Send notifications through multiple channels
48
- #
49
- # @param approval [Approval] The approval request
50
- # @param message [String] The notification message
51
- # @param channels [Array<Symbol>] The channels to notify
52
- # @return [Hash<Symbol, Boolean>] Results per channel
53
- def notify(approval, message, channels:)
54
- Registry.notify_all(approval, message, channels: channels)
55
- end
56
-
57
- # Reset all notifier configuration
58
- #
59
- # @return [void]
60
- def reset!
61
- Registry.reset!
62
- Email.reset!
63
- Slack.reset!
64
- Webhook.reset!
65
- end
66
- end
67
- end
68
- end
69
- end
70
- end
@@ -1,416 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative "result"
4
- require_relative "instrumentation"
5
- require_relative "thread_pool"
6
- require_relative "dsl"
7
- require_relative "dsl/executor"
8
-
9
- module RubyLLM
10
- module Agents
11
- # Base class for workflow orchestration
12
- #
13
- # Provides shared functionality for composing multiple agents into
14
- # coordinated workflows using the DSL:
15
- # - Sequential steps with data flowing between them
16
- # - Parallel execution with result aggregation
17
- # - Conditional routing based on step results
18
- #
19
- # @example Minimal workflow
20
- # class SimpleWorkflow < RubyLLM::Agents::Workflow
21
- # step :fetch, FetcherAgent
22
- # step :process, ProcessorAgent
23
- # step :save, SaverAgent
24
- # end
25
- #
26
- # @example Full-featured workflow
27
- # class OrderWorkflow < RubyLLM::Agents::Workflow
28
- # description "Process customer orders end-to-end"
29
- #
30
- # input do
31
- # required :order_id, String
32
- # optional :priority, String, default: "normal"
33
- # end
34
- #
35
- # step :fetch, FetcherAgent, timeout: 1.minute
36
- # step :validate, ValidatorAgent
37
- #
38
- # step :process, on: -> { validate.tier } do |route|
39
- # route.premium PremiumAgent
40
- # route.standard StandardAgent
41
- # route.default DefaultAgent
42
- # end
43
- #
44
- # parallel do
45
- # step :analyze, AnalyzerAgent
46
- # step :summarize, SummarizerAgent
47
- # end
48
- #
49
- # step :notify, NotifierAgent, if: :should_notify?
50
- #
51
- # private
52
- #
53
- # def should_notify?
54
- # input.callback_url.present?
55
- # end
56
- # end
57
- #
58
- # @api public
59
- class Workflow
60
- include Workflow::Instrumentation
61
- include Workflow::DSL
62
-
63
- class << self
64
- # @!attribute [rw] version
65
- # @return [String] Version identifier for the workflow
66
- attr_accessor :_version
67
-
68
- # @!attribute [rw] timeout
69
- # @return [Integer, nil] Total timeout for the entire workflow in seconds
70
- attr_accessor :_timeout
71
-
72
- # @!attribute [rw] max_cost
73
- # @return [Float, nil] Maximum cost threshold for the workflow
74
- attr_accessor :_max_cost
75
-
76
- # @!attribute [rw] description
77
- # @return [String, nil] Description of the workflow
78
- attr_accessor :_description
79
-
80
- # @!attribute [rw] max_recursion_depth
81
- # @return [Integer] Maximum recursion depth for self-referential workflows
82
- attr_accessor :_max_recursion_depth
83
-
84
- # Sets or returns the workflow version
85
- #
86
- # @param value [String, nil] Version string to set
87
- # @return [String] The current version
88
- def version(value = nil)
89
- if value
90
- self._version = value
91
- else
92
- _version || "1.0"
93
- end
94
- end
95
-
96
- # Sets or returns the workflow timeout
97
- #
98
- # @param value [Integer, ActiveSupport::Duration, nil] Timeout to set
99
- # @return [Integer, nil] The current timeout in seconds
100
- def timeout(value = nil)
101
- if value
102
- self._timeout = value.is_a?(ActiveSupport::Duration) ? value.to_i : value
103
- else
104
- _timeout
105
- end
106
- end
107
-
108
- # Sets or returns the maximum cost threshold
109
- #
110
- # @param value [Float, nil] Max cost in USD
111
- # @return [Float, nil] The current max cost
112
- def max_cost(value = nil)
113
- if value
114
- self._max_cost = value.to_f
115
- else
116
- _max_cost
117
- end
118
- end
119
-
120
- # Sets or returns the workflow description
121
- #
122
- # @param value [String, nil] Description text to set
123
- # @return [String, nil] The current description
124
- def description(value = nil)
125
- if value
126
- self._description = value
127
- else
128
- _description
129
- end
130
- end
131
-
132
- # Sets or returns the maximum recursion depth
133
- #
134
- # @param value [Integer, nil] Max depth to set
135
- # @return [Integer] The current max recursion depth (default: 10)
136
- def max_recursion_depth(value = nil)
137
- if value
138
- self._max_recursion_depth = value.to_i
139
- else
140
- _max_recursion_depth || 10
141
- end
142
- end
143
-
144
- # Factory method to instantiate and execute a workflow
145
- #
146
- # Supports both hash and keyword argument styles:
147
- # MyWorkflow.call(order_id: "123")
148
- # MyWorkflow.call({ order_id: "123" })
149
- #
150
- # @param input [Hash] Input hash (optional)
151
- # @param kwargs [Hash] Parameters to pass to the workflow
152
- # @yield [chunk] Optional block for streaming support
153
- # @return [WorkflowResult] The workflow result with aggregate metrics
154
- def call(input = nil, **kwargs, &block)
155
- # Support both call(hash) and call(**kwargs) patterns
156
- merged_input = input.is_a?(Hash) ? input.merge(kwargs) : kwargs
157
- # Pass input to constructor to maintain backward compatibility with
158
- # legacy subclasses that override call without arguments
159
- new(**merged_input).call(&block)
160
- end
161
- end
162
-
163
- # @!attribute [r] options
164
- # @return [Hash] The options passed to the workflow
165
- attr_reader :options
166
-
167
- # @!attribute [r] workflow_id
168
- # @return [String] Unique identifier for this workflow execution
169
- attr_reader :workflow_id
170
-
171
- # @!attribute [r] execution_id
172
- # @return [Integer, nil] The ID of the root execution record
173
- attr_reader :execution_id
174
-
175
- # @!attribute [r] step_results
176
- # @return [Hash<Symbol, Result>] Results from executed steps
177
- attr_reader :step_results
178
-
179
- # @!attribute [r] recursion_depth
180
- # @return [Integer] Current recursion depth for self-referential workflows
181
- attr_reader :recursion_depth
182
-
183
- # Creates a new workflow instance
184
- #
185
- # @param kwargs [Hash] Parameters for the workflow
186
- def initialize(**kwargs)
187
- @options = kwargs
188
- @workflow_id = SecureRandom.uuid
189
- @execution_id = nil
190
- @accumulated_cost = 0.0
191
- @step_results = {}
192
- @validated_input = nil
193
-
194
- # Extract recursion context from execution_metadata
195
- metadata = kwargs[:execution_metadata] || {}
196
- @recursion_depth = metadata[:recursion_depth] || 0
197
- @remaining_timeout = metadata[:remaining_timeout]
198
- @remaining_cost_budget = metadata[:remaining_cost_budget]
199
-
200
- # Check recursion depth
201
- check_recursion_depth!
202
- end
203
-
204
- # Executes the workflow
205
- #
206
- # When using the new DSL with `step` declarations, this method
207
- # automatically executes the workflow using the DSL executor.
208
- # For legacy subclasses (Pipeline, Parallel, Router), this raises
209
- # NotImplementedError to be overridden.
210
- #
211
- # Supports both hash and keyword argument styles:
212
- # workflow.call(order_id: "123")
213
- # workflow.call({ order_id: "123" })
214
- #
215
- # @param input [Hash] Input hash (optional)
216
- # @param kwargs [Hash] Keyword arguments for input
217
- # @yield [chunk] Optional block for streaming support
218
- # @return [WorkflowResult] The workflow result
219
- def call(input = nil, **kwargs, &block)
220
- # Merge input sources: constructor options, hash arg, keyword args
221
- merged_input = @options.merge(input.is_a?(Hash) ? input : {}).merge(kwargs)
222
- @options = merged_input
223
-
224
- # Use DSL executor if steps are defined with the new DSL
225
- if self.class.step_configs.any?
226
- instrument_workflow do
227
- execute_with_dsl(&block)
228
- end
229
- else
230
- raise NotImplementedError, "#{self.class} must implement #call or define steps"
231
- end
232
- end
233
-
234
- # Validates workflow input and executes a dry run
235
- #
236
- # Returns information about the workflow without executing agents.
237
- # Supports both positional hash and keyword arguments.
238
- #
239
- # @param input_hash [Hash] Input hash (optional)
240
- # @param input [Hash] Keyword arguments for input
241
- # @return [Hash] Validation results and workflow structure
242
- def self.dry_run(input_hash = nil, **input)
243
- input = input_hash.merge(input) if input_hash.is_a?(Hash)
244
- errors = []
245
-
246
- # Validate input if schema defined
247
- if input_schema
248
- begin
249
- input_schema.validate!(input)
250
- rescue DSL::InputSchema::ValidationError => e
251
- errors.concat(e.errors)
252
- end
253
- end
254
-
255
- # Validate configuration
256
- errors.concat(validate_configuration)
257
-
258
- {
259
- valid: errors.empty?,
260
- input_errors: errors,
261
- steps: step_metadata.map { |s| s[:name] },
262
- agents: step_metadata.map { |s| s[:agent] }.compact,
263
- parallel_groups: parallel_groups.map(&:to_h),
264
- warnings: validate_configuration
265
- }
266
- end
267
-
268
- private
269
-
270
- # Executes the workflow using the DSL executor
271
- #
272
- # @return [WorkflowResult] The workflow result
273
- def execute_with_dsl(&block)
274
- executor = DSL::Executor.new(self)
275
- executor.execute(&block)
276
- end
277
-
278
- public
279
-
280
- protected
281
-
282
- # Executes a single agent within the workflow context
283
- #
284
- # Passes execution metadata for proper tracking and hierarchy.
285
- #
286
- # @param agent_class [Class] The agent class to execute
287
- # @param input [Hash] Parameters to pass to the agent
288
- # @param step_name [String, Symbol] Name of the workflow step
289
- # @yield [chunk] Optional block for streaming
290
- # @return [Result] The agent result
291
- def execute_agent(agent_class, input, step_name: nil, &block)
292
- metadata = {
293
- parent_execution_id: execution_id,
294
- root_execution_id: root_execution_id,
295
- workflow_id: workflow_id,
296
- workflow_type: self.class.name,
297
- workflow_step: step_name&.to_s
298
- }.compact
299
-
300
- # Merge workflow metadata with any existing metadata
301
- merged_input = input.merge(
302
- execution_metadata: metadata.merge(input[:execution_metadata] || {})
303
- )
304
-
305
- result = agent_class.call(**merged_input, &block)
306
-
307
- # Track accumulated cost for max_cost enforcement
308
- @accumulated_cost += result.total_cost if result.respond_to?(:total_cost) && result.total_cost
309
-
310
- # Check cost threshold
311
- check_cost_threshold!
312
-
313
- result
314
- end
315
-
316
- # Returns the root execution ID for the workflow
317
- #
318
- # @return [Integer, nil] The root execution ID
319
- def root_execution_id
320
- @root_execution_id || execution_id
321
- end
322
-
323
- # Sets the root execution ID
324
- #
325
- # @param id [Integer] The root execution ID
326
- def root_execution_id=(id)
327
- @root_execution_id = id
328
- end
329
-
330
- # Checks if accumulated cost exceeds the threshold
331
- #
332
- # @raise [WorkflowCostExceededError] If cost exceeds max_cost
333
- def check_cost_threshold!
334
- # Check against remaining budget if we're in a sub-workflow
335
- effective_max = @remaining_cost_budget || self.class.max_cost
336
- return unless effective_max
337
- return if @accumulated_cost <= effective_max
338
-
339
- raise WorkflowCostExceededError.new(
340
- "Workflow cost ($#{@accumulated_cost.round(4)}) exceeded maximum ($#{effective_max})",
341
- accumulated_cost: @accumulated_cost,
342
- max_cost: effective_max
343
- )
344
- end
345
-
346
- # Checks if recursion depth exceeds the maximum
347
- #
348
- # @raise [RecursionDepthExceededError] If depth exceeds max
349
- def check_recursion_depth!
350
- max_depth = self.class.max_recursion_depth
351
- return if @recursion_depth <= max_depth
352
-
353
- raise RecursionDepthExceededError.new(
354
- "Workflow recursion depth (#{@recursion_depth}) exceeded maximum (#{max_depth})",
355
- current_depth: @recursion_depth,
356
- max_depth: max_depth
357
- )
358
- end
359
-
360
- # Hook for subclasses to transform input before a step
361
- #
362
- # @param step_name [Symbol] The step name
363
- # @param context [Hash] Current workflow context
364
- # @return [Hash] Transformed input for the step
365
- def before_step(step_name, context)
366
- method_name = :"before_#{step_name}"
367
- if respond_to?(method_name, true)
368
- send(method_name, context)
369
- else
370
- extract_step_input(context)
371
- end
372
- end
373
-
374
- # Extracts input for the next step from context
375
- #
376
- # Default behavior: use the last step's content or original input
377
- #
378
- # @param context [Hash] Current workflow context
379
- # @return [Hash] Input for the next step
380
- def extract_step_input(context)
381
- # Get the last non-input result
382
- last_result = context.except(:input).values.last
383
-
384
- if last_result.is_a?(Result) || last_result.is_a?(Workflow::Result)
385
- # If content is a hash, use it; otherwise wrap it
386
- content = last_result.content
387
- content.is_a?(Hash) ? content : { input: content }
388
- else
389
- context[:input] || {}
390
- end
391
- end
392
- end
393
-
394
- # Error raised when workflow cost exceeds the configured maximum
395
- class WorkflowCostExceededError < StandardError
396
- attr_reader :accumulated_cost, :max_cost
397
-
398
- def initialize(message, accumulated_cost:, max_cost:)
399
- super(message)
400
- @accumulated_cost = accumulated_cost
401
- @max_cost = max_cost
402
- end
403
- end
404
-
405
- # Error raised when workflow recursion depth exceeds the maximum
406
- class RecursionDepthExceededError < StandardError
407
- attr_reader :current_depth, :max_depth
408
-
409
- def initialize(message, current_depth:, max_depth:)
410
- super(message)
411
- @current_depth = current_depth
412
- @max_depth = max_depth
413
- end
414
- end
415
- end
416
- end