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,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
|