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.
Files changed (152) hide show
  1. checksums.yaml +4 -4
  2. data/app/controllers/concerns/ruby_llm/agents/paginatable.rb +9 -3
  3. data/app/controllers/concerns/ruby_llm/agents/sortable.rb +58 -0
  4. data/app/controllers/ruby_llm/agents/agents_controller.rb +59 -16
  5. data/app/controllers/ruby_llm/agents/dashboard_controller.rb +144 -20
  6. data/app/controllers/ruby_llm/agents/executions_controller.rb +13 -16
  7. data/app/controllers/ruby_llm/agents/workflows_controller.rb +279 -90
  8. data/app/helpers/ruby_llm/agents/application_helper.rb +100 -0
  9. data/app/mailers/ruby_llm/agents/alert_mailer.rb +84 -0
  10. data/app/mailers/ruby_llm/agents/application_mailer.rb +28 -0
  11. data/app/models/ruby_llm/agents/execution/analytics.rb +170 -20
  12. data/app/models/ruby_llm/agents/execution/scopes.rb +0 -31
  13. data/app/models/ruby_llm/agents/execution/workflow.rb +0 -129
  14. data/app/models/ruby_llm/agents/execution.rb +50 -14
  15. data/app/models/ruby_llm/agents/tenant/budgetable.rb +277 -0
  16. data/app/models/ruby_llm/agents/tenant/configurable.rb +135 -0
  17. data/app/models/ruby_llm/agents/tenant/trackable.rb +310 -0
  18. data/app/models/ruby_llm/agents/tenant.rb +146 -0
  19. data/app/models/ruby_llm/agents/tenant_budget.rb +12 -253
  20. data/app/services/ruby_llm/agents/agent_registry.rb +18 -12
  21. data/app/views/layouts/ruby_llm/agents/application.html.erb +72 -76
  22. data/app/views/ruby_llm/agents/agents/_agent.html.erb +0 -12
  23. data/app/views/ruby_llm/agents/agents/_sortable_header.html.erb +56 -0
  24. data/app/views/ruby_llm/agents/agents/_workflow.html.erb +5 -15
  25. data/app/views/ruby_llm/agents/agents/index.html.erb +271 -100
  26. data/app/views/ruby_llm/agents/agents/show.html.erb +1 -0
  27. data/app/views/ruby_llm/agents/alert_mailer/alert_notification.html.erb +107 -0
  28. data/app/views/ruby_llm/agents/alert_mailer/alert_notification.text.erb +18 -0
  29. data/app/views/ruby_llm/agents/api_configurations/show.html.erb +4 -1
  30. data/app/views/ruby_llm/agents/dashboard/_agent_comparison.html.erb +66 -359
  31. data/app/views/ruby_llm/agents/dashboard/_model_comparison.html.erb +56 -0
  32. data/app/views/ruby_llm/agents/dashboard/_model_cost_breakdown.html.erb +115 -0
  33. data/app/views/ruby_llm/agents/dashboard/_now_strip.html.erb +35 -60
  34. data/app/views/ruby_llm/agents/dashboard/_top_errors.html.erb +17 -6
  35. data/app/views/ruby_llm/agents/dashboard/index.html.erb +373 -72
  36. data/app/views/ruby_llm/agents/executions/_execution.html.erb +0 -1
  37. data/app/views/ruby_llm/agents/executions/_filters.html.erb +51 -39
  38. data/app/views/ruby_llm/agents/executions/_list.html.erb +53 -195
  39. data/app/views/ruby_llm/agents/executions/_workflow_summary.html.erb +5 -20
  40. data/app/views/ruby_llm/agents/executions/index.html.erb +7 -83
  41. data/app/views/ruby_llm/agents/executions/show.html.erb +10 -20
  42. data/app/views/ruby_llm/agents/shared/_agent_type_badge.html.erb +2 -1
  43. data/app/views/ruby_llm/agents/shared/_doc_link.html.erb +12 -0
  44. data/app/views/ruby_llm/agents/shared/_executions_table.html.erb +3 -15
  45. data/app/views/ruby_llm/agents/shared/_filter_dropdown.html.erb +1 -1
  46. data/app/views/ruby_llm/agents/shared/_select_dropdown.html.erb +1 -1
  47. data/app/views/ruby_llm/agents/shared/_sortable_header.html.erb +53 -0
  48. data/app/views/ruby_llm/agents/shared/_status_badge.html.erb +7 -0
  49. data/app/views/ruby_llm/agents/shared/_status_dot.html.erb +1 -1
  50. data/app/views/ruby_llm/agents/shared/_workflow_type_badge.html.erb +9 -35
  51. data/app/views/ruby_llm/agents/system_config/show.html.erb +4 -1
  52. data/app/views/ruby_llm/agents/tenants/index.html.erb +4 -1
  53. data/app/views/ruby_llm/agents/workflows/_step_performance.html.erb +7 -15
  54. data/app/views/ruby_llm/agents/workflows/_structure_dsl.html.erb +539 -0
  55. data/app/views/ruby_llm/agents/workflows/_workflow_diagram.html.erb +920 -0
  56. data/app/views/ruby_llm/agents/workflows/index.html.erb +179 -0
  57. data/app/views/ruby_llm/agents/workflows/show.html.erb +164 -139
  58. data/config/routes.rb +1 -1
  59. data/lib/generators/ruby_llm_agents/agent_generator.rb +6 -36
  60. data/lib/generators/ruby_llm_agents/background_remover_generator.rb +7 -37
  61. data/lib/generators/ruby_llm_agents/embedder_generator.rb +5 -38
  62. data/lib/generators/ruby_llm_agents/image_analyzer_generator.rb +7 -37
  63. data/lib/generators/ruby_llm_agents/image_editor_generator.rb +7 -37
  64. data/lib/generators/ruby_llm_agents/image_generator_generator.rb +8 -41
  65. data/lib/generators/ruby_llm_agents/image_pipeline_generator.rb +18 -46
  66. data/lib/generators/ruby_llm_agents/image_transformer_generator.rb +7 -37
  67. data/lib/generators/ruby_llm_agents/image_upscaler_generator.rb +7 -37
  68. data/lib/generators/ruby_llm_agents/image_variator_generator.rb +7 -37
  69. data/lib/generators/ruby_llm_agents/install_generator.rb +33 -56
  70. data/lib/generators/ruby_llm_agents/migrate_structure_generator.rb +480 -0
  71. data/lib/generators/ruby_llm_agents/multi_tenancy_generator.rb +42 -22
  72. data/lib/generators/ruby_llm_agents/restructure_generator.rb +2 -2
  73. data/lib/generators/ruby_llm_agents/speaker_generator.rb +8 -39
  74. data/lib/generators/ruby_llm_agents/templates/add_tenant_to_executions_migration.rb.tt +13 -2
  75. data/lib/generators/ruby_llm_agents/templates/agent.rb.tt +5 -8
  76. data/lib/generators/ruby_llm_agents/templates/application_agent.rb.tt +40 -42
  77. data/lib/generators/ruby_llm_agents/templates/application_background_remover.rb.tt +20 -22
  78. data/lib/generators/ruby_llm_agents/templates/application_embedder.rb.tt +24 -26
  79. data/lib/generators/ruby_llm_agents/templates/application_image_analyzer.rb.tt +20 -22
  80. data/lib/generators/ruby_llm_agents/templates/application_image_editor.rb.tt +19 -17
  81. data/lib/generators/ruby_llm_agents/templates/application_image_generator.rb.tt +31 -33
  82. data/lib/generators/ruby_llm_agents/templates/application_image_pipeline.rb.tt +125 -127
  83. data/lib/generators/ruby_llm_agents/templates/application_image_transformer.rb.tt +20 -18
  84. data/lib/generators/ruby_llm_agents/templates/application_image_upscaler.rb.tt +19 -17
  85. data/lib/generators/ruby_llm_agents/templates/application_image_variator.rb.tt +19 -17
  86. data/lib/generators/ruby_llm_agents/templates/application_speaker.rb.tt +38 -40
  87. data/lib/generators/ruby_llm_agents/templates/application_transcriber.rb.tt +42 -44
  88. data/lib/generators/ruby_llm_agents/templates/application_workflow.rb.tt +48 -0
  89. data/lib/generators/ruby_llm_agents/templates/background_remover.rb.tt +19 -21
  90. data/lib/generators/ruby_llm_agents/templates/create_tenant_budgets_migration.rb.tt +11 -0
  91. data/lib/generators/ruby_llm_agents/templates/create_tenants_migration.rb.tt +72 -0
  92. data/lib/generators/ruby_llm_agents/templates/embedder.rb.tt +19 -21
  93. data/lib/generators/ruby_llm_agents/templates/image_analyzer.rb.tt +20 -22
  94. data/lib/generators/ruby_llm_agents/templates/image_editor.rb.tt +15 -17
  95. data/lib/generators/ruby_llm_agents/templates/image_generator.rb.tt +25 -27
  96. data/lib/generators/ruby_llm_agents/templates/image_pipeline.rb.tt +19 -21
  97. data/lib/generators/ruby_llm_agents/templates/image_transformer.rb.tt +20 -22
  98. data/lib/generators/ruby_llm_agents/templates/image_upscaler.rb.tt +17 -19
  99. data/lib/generators/ruby_llm_agents/templates/image_variator.rb.tt +15 -17
  100. data/lib/generators/ruby_llm_agents/templates/rename_tenant_budgets_to_tenants_migration.rb.tt +34 -0
  101. data/lib/generators/ruby_llm_agents/templates/skills/AGENTS.md.tt +87 -24
  102. data/lib/generators/ruby_llm_agents/templates/skills/BACKGROUND_REMOVERS.md.tt +21 -27
  103. data/lib/generators/ruby_llm_agents/templates/skills/EMBEDDERS.md.tt +46 -54
  104. data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_ANALYZERS.md.tt +31 -39
  105. data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_EDITORS.md.tt +22 -28
  106. data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_GENERATORS.md.tt +53 -63
  107. data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_PIPELINES.md.tt +46 -56
  108. data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_TRANSFORMERS.md.tt +23 -31
  109. data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_UPSCALERS.md.tt +22 -30
  110. data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_VARIATORS.md.tt +23 -31
  111. data/lib/generators/ruby_llm_agents/templates/skills/SPEAKERS.md.tt +38 -46
  112. data/lib/generators/ruby_llm_agents/templates/skills/TOOLS.md.tt +7 -7
  113. data/lib/generators/ruby_llm_agents/templates/skills/TRANSCRIBERS.md.tt +59 -71
  114. data/lib/generators/ruby_llm_agents/templates/skills/WORKFLOWS.md.tt +274 -23
  115. data/lib/generators/ruby_llm_agents/templates/speaker.rb.tt +29 -31
  116. data/lib/generators/ruby_llm_agents/templates/transcriber.rb.tt +28 -30
  117. data/lib/generators/ruby_llm_agents/transcriber_generator.rb +10 -43
  118. data/lib/generators/ruby_llm_agents/upgrade_generator.rb +26 -0
  119. data/lib/ruby_llm/agents/core/configuration.rb +55 -43
  120. data/lib/ruby_llm/agents/core/llm_tenant.rb +60 -60
  121. data/lib/ruby_llm/agents/core/version.rb +1 -1
  122. data/lib/ruby_llm/agents/infrastructure/alert_manager.rb +26 -0
  123. data/lib/ruby_llm/agents/infrastructure/budget/config_resolver.rb +4 -2
  124. data/lib/ruby_llm/agents/pipeline.rb +69 -0
  125. data/lib/ruby_llm/agents/workflow/approval.rb +205 -0
  126. data/lib/ruby_llm/agents/workflow/approval_store.rb +179 -0
  127. data/lib/ruby_llm/agents/workflow/dsl/executor.rb +467 -0
  128. data/lib/ruby_llm/agents/workflow/dsl/input_schema.rb +244 -0
  129. data/lib/ruby_llm/agents/workflow/dsl/iteration_executor.rb +289 -0
  130. data/lib/ruby_llm/agents/workflow/dsl/parallel_group.rb +107 -0
  131. data/lib/ruby_llm/agents/workflow/dsl/route_builder.rb +150 -0
  132. data/lib/ruby_llm/agents/workflow/dsl/schedule_helpers.rb +187 -0
  133. data/lib/ruby_llm/agents/workflow/dsl/step_config.rb +352 -0
  134. data/lib/ruby_llm/agents/workflow/dsl/step_executor.rb +415 -0
  135. data/lib/ruby_llm/agents/workflow/dsl/wait_config.rb +257 -0
  136. data/lib/ruby_llm/agents/workflow/dsl/wait_executor.rb +317 -0
  137. data/lib/ruby_llm/agents/workflow/dsl.rb +576 -0
  138. data/lib/ruby_llm/agents/workflow/instrumentation.rb +2 -7
  139. data/lib/ruby_llm/agents/workflow/notifiers/base.rb +117 -0
  140. data/lib/ruby_llm/agents/workflow/notifiers/email.rb +117 -0
  141. data/lib/ruby_llm/agents/workflow/notifiers/slack.rb +180 -0
  142. data/lib/ruby_llm/agents/workflow/notifiers/webhook.rb +121 -0
  143. data/lib/ruby_llm/agents/workflow/notifiers.rb +70 -0
  144. data/lib/ruby_llm/agents/workflow/orchestrator.rb +190 -23
  145. data/lib/ruby_llm/agents/workflow/result.rb +202 -0
  146. data/lib/ruby_llm/agents/workflow/throttle_manager.rb +206 -0
  147. data/lib/ruby_llm/agents/workflow/wait_result.rb +213 -0
  148. metadata +43 -6
  149. data/app/views/ruby_llm/agents/dashboard/_execution_item.html.erb +0 -66
  150. data/lib/ruby_llm/agents/workflow/parallel.rb +0 -299
  151. data/lib/ruby_llm/agents/workflow/pipeline.rb +0 -306
  152. data/lib/ruby_llm/agents/workflow/router.rb +0 -429
@@ -0,0 +1,117 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module Agents
5
+ class Workflow
6
+ module Notifiers
7
+ # Email notification adapter for approval requests
8
+ #
9
+ # Uses ActionMailer if available, or a configured mailer class.
10
+ # Can be configured with custom templates and delivery options.
11
+ #
12
+ # @example Configuration
13
+ # RubyLLM::Agents::Workflow::Notifiers::Email.configure do |config|
14
+ # config.mailer_class = ApprovalMailer
15
+ # config.from = "approvals@example.com"
16
+ # end
17
+ #
18
+ # @api public
19
+ class Email < Base
20
+ class << self
21
+ attr_accessor :mailer_class, :from_address, :subject_prefix
22
+
23
+ # Configure the email notifier
24
+ #
25
+ # @yield [self] The email notifier class
26
+ # @return [void]
27
+ def configure
28
+ yield self
29
+ end
30
+
31
+ # Reset configuration to defaults
32
+ #
33
+ # @return [void]
34
+ def reset!
35
+ @mailer_class = nil
36
+ @from_address = nil
37
+ @subject_prefix = nil
38
+ end
39
+ end
40
+
41
+ # @param mailer_class [Class, nil] Custom mailer class
42
+ # @param from [String, nil] From address
43
+ # @param subject_prefix [String, nil] Subject line prefix
44
+ def initialize(mailer_class: nil, from: nil, subject_prefix: nil)
45
+ @mailer_class = mailer_class || self.class.mailer_class
46
+ @from_address = from || self.class.from_address || "noreply@example.com"
47
+ @subject_prefix = subject_prefix || self.class.subject_prefix || "[Approval Required]"
48
+ end
49
+
50
+ # Send an email notification
51
+ #
52
+ # @param approval [Approval] The approval request
53
+ # @param message [String] The notification message
54
+ # @return [Boolean] true if email was queued
55
+ def notify(approval, message)
56
+ if @mailer_class
57
+ send_via_mailer(approval, message)
58
+ elsif defined?(ActionMailer)
59
+ send_via_action_mailer(approval, message)
60
+ else
61
+ log_notification(approval, message)
62
+ false
63
+ end
64
+ rescue StandardError => e
65
+ handle_error(e, approval)
66
+ false
67
+ end
68
+
69
+ private
70
+
71
+ def send_via_mailer(approval, message)
72
+ if @mailer_class.respond_to?(:approval_request)
73
+ mail = @mailer_class.approval_request(approval, message)
74
+ deliver_mail(mail)
75
+ true
76
+ else
77
+ false
78
+ end
79
+ end
80
+
81
+ def send_via_action_mailer(approval, message)
82
+ # Generic ActionMailer support if no custom mailer is configured
83
+ # Applications should configure a mailer_class for production use
84
+ log_notification(approval, message)
85
+ false
86
+ end
87
+
88
+ def deliver_mail(mail)
89
+ if mail.respond_to?(:deliver_later)
90
+ mail.deliver_later
91
+ elsif mail.respond_to?(:deliver_now)
92
+ mail.deliver_now
93
+ elsif mail.respond_to?(:deliver)
94
+ mail.deliver
95
+ end
96
+ end
97
+
98
+ def log_notification(approval, message)
99
+ if defined?(Rails) && Rails.logger
100
+ Rails.logger.info(
101
+ "[RubyLLM::Agents] Email notification for approval #{approval.id}: #{message}"
102
+ )
103
+ end
104
+ end
105
+
106
+ def handle_error(error, approval)
107
+ if defined?(Rails) && Rails.logger
108
+ Rails.logger.error(
109
+ "[RubyLLM::Agents] Failed to send email for approval #{approval.id}: #{error.message}"
110
+ )
111
+ end
112
+ end
113
+ end
114
+ end
115
+ end
116
+ end
117
+ end
@@ -0,0 +1,180 @@
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
+ # Slack notification adapter for approval requests
12
+ #
13
+ # Sends notifications via Slack webhooks or the Slack API.
14
+ # Supports rich message formatting with blocks.
15
+ #
16
+ # @example Using a webhook
17
+ # notifier = Slack.new(webhook_url: "https://hooks.slack.com/...")
18
+ # notifier.notify(approval, "Please review this request")
19
+ #
20
+ # @example Using the API
21
+ # notifier = Slack.new(api_token: "xoxb-...", channel: "#approvals")
22
+ #
23
+ # @api public
24
+ class Slack < Base
25
+ class << self
26
+ attr_accessor :webhook_url, :api_token, :default_channel
27
+
28
+ # Configure the Slack notifier
29
+ #
30
+ # @yield [self] The Slack notifier class
31
+ # @return [void]
32
+ def configure
33
+ yield self
34
+ end
35
+
36
+ # Reset configuration to defaults
37
+ #
38
+ # @return [void]
39
+ def reset!
40
+ @webhook_url = nil
41
+ @api_token = nil
42
+ @default_channel = nil
43
+ end
44
+ end
45
+
46
+ # @param webhook_url [String, nil] Slack webhook URL
47
+ # @param api_token [String, nil] Slack API token (for posting via API)
48
+ # @param channel [String, nil] Default channel for messages
49
+ def initialize(webhook_url: nil, api_token: nil, channel: nil)
50
+ @webhook_url = webhook_url || self.class.webhook_url
51
+ @api_token = api_token || self.class.api_token
52
+ @channel = channel || self.class.default_channel
53
+ end
54
+
55
+ # Send a Slack notification
56
+ #
57
+ # @param approval [Approval] The approval request
58
+ # @param message [String] The notification message
59
+ # @return [Boolean] true if notification was sent
60
+ def notify(approval, message)
61
+ payload = build_payload(approval, message)
62
+
63
+ if @webhook_url
64
+ send_webhook(payload)
65
+ elsif @api_token
66
+ send_api(payload)
67
+ else
68
+ log_notification(approval, message)
69
+ false
70
+ end
71
+ rescue StandardError => e
72
+ handle_error(e, approval)
73
+ false
74
+ end
75
+
76
+ private
77
+
78
+ def build_payload(approval, message)
79
+ {
80
+ text: message,
81
+ blocks: build_blocks(approval, message)
82
+ }.tap do |payload|
83
+ payload[:channel] = @channel if @channel && @api_token
84
+ end
85
+ end
86
+
87
+ def build_blocks(approval, message)
88
+ [
89
+ {
90
+ type: "header",
91
+ text: {
92
+ type: "plain_text",
93
+ text: "Approval Required: #{approval.name}",
94
+ emoji: true
95
+ }
96
+ },
97
+ {
98
+ type: "section",
99
+ text: {
100
+ type: "mrkdwn",
101
+ text: message
102
+ }
103
+ },
104
+ {
105
+ type: "section",
106
+ fields: [
107
+ {
108
+ type: "mrkdwn",
109
+ text: "*Workflow:*\n#{approval.workflow_type}"
110
+ },
111
+ {
112
+ type: "mrkdwn",
113
+ text: "*Workflow ID:*\n#{approval.workflow_id}"
114
+ }
115
+ ]
116
+ },
117
+ {
118
+ type: "context",
119
+ elements: [
120
+ {
121
+ type: "mrkdwn",
122
+ text: "Approval ID: `#{approval.id}`"
123
+ }
124
+ ]
125
+ }
126
+ ]
127
+ end
128
+
129
+ def send_webhook(payload)
130
+ uri = URI.parse(@webhook_url)
131
+ http = Net::HTTP.new(uri.host, uri.port)
132
+ http.use_ssl = uri.scheme == "https"
133
+ http.open_timeout = 5
134
+ http.read_timeout = 10
135
+
136
+ request = Net::HTTP::Post.new(uri.path)
137
+ request["Content-Type"] = "application/json"
138
+ request.body = payload.to_json
139
+
140
+ response = http.request(request)
141
+ response.code.to_i == 200
142
+ end
143
+
144
+ def send_api(payload)
145
+ uri = URI.parse("https://slack.com/api/chat.postMessage")
146
+ http = Net::HTTP.new(uri.host, uri.port)
147
+ http.use_ssl = true
148
+ http.open_timeout = 5
149
+ http.read_timeout = 10
150
+
151
+ request = Net::HTTP::Post.new(uri.path)
152
+ request["Content-Type"] = "application/json"
153
+ request["Authorization"] = "Bearer #{@api_token}"
154
+ request.body = payload.to_json
155
+
156
+ response = http.request(request)
157
+ result = JSON.parse(response.body)
158
+ result["ok"] == true
159
+ end
160
+
161
+ def log_notification(approval, message)
162
+ if defined?(Rails) && Rails.logger
163
+ Rails.logger.info(
164
+ "[RubyLLM::Agents] Slack notification for approval #{approval.id}: #{message}"
165
+ )
166
+ end
167
+ end
168
+
169
+ def handle_error(error, approval)
170
+ if defined?(Rails) && Rails.logger
171
+ Rails.logger.error(
172
+ "[RubyLLM::Agents] Failed to send Slack message for approval #{approval.id}: #{error.message}"
173
+ )
174
+ end
175
+ end
176
+ end
177
+ end
178
+ end
179
+ end
180
+ end
@@ -0,0 +1,121 @@
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
@@ -0,0 +1,70 @@
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