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.
- checksums.yaml +4 -4
- data/README.md +101 -334
- data/app/controllers/concerns/ruby_llm/agents/sortable.rb +0 -1
- data/app/controllers/ruby_llm/agents/agents_controller.rb +5 -56
- data/app/controllers/ruby_llm/agents/dashboard_controller.rb +22 -106
- data/app/controllers/ruby_llm/agents/executions_controller.rb +4 -114
- data/app/controllers/ruby_llm/agents/tenants_controller.rb +30 -2
- data/app/helpers/ruby_llm/agents/application_helper.rb +19 -53
- data/app/models/ruby_llm/agents/execution/analytics.rb +13 -54
- data/app/models/ruby_llm/agents/execution/scopes.rb +61 -14
- data/app/models/ruby_llm/agents/execution.rb +46 -10
- data/app/models/ruby_llm/agents/execution_detail.rb +18 -0
- data/app/models/ruby_llm/agents/tenant/budgetable.rb +132 -24
- data/app/models/ruby_llm/agents/tenant/incrementable.rb +117 -0
- data/app/models/ruby_llm/agents/tenant/resettable.rb +128 -0
- data/app/models/ruby_llm/agents/tenant/trackable.rb +46 -12
- data/app/models/ruby_llm/agents/tenant.rb +2 -3
- data/app/models/ruby_llm/agents/tenant_budget.rb +6 -3
- data/app/services/ruby_llm/agents/agent_registry.rb +6 -112
- data/app/views/layouts/ruby_llm/agents/application.html.erb +87 -252
- data/app/views/ruby_llm/agents/agents/_config_agent.html.erb +71 -218
- data/app/views/ruby_llm/agents/agents/_config_embedder.html.erb +20 -63
- data/app/views/ruby_llm/agents/agents/_config_image_generator.html.erb +44 -131
- data/app/views/ruby_llm/agents/agents/_config_moderator.html.erb +16 -57
- data/app/views/ruby_llm/agents/agents/_config_speaker.html.erb +39 -104
- data/app/views/ruby_llm/agents/agents/_config_transcriber.html.erb +29 -82
- data/app/views/ruby_llm/agents/agents/_empty_state.html.erb +4 -14
- data/app/views/ruby_llm/agents/agents/index.html.erb +105 -274
- data/app/views/ruby_llm/agents/agents/show.html.erb +248 -378
- data/app/views/ruby_llm/agents/dashboard/_action_center.html.erb +29 -52
- data/app/views/ruby_llm/agents/dashboard/_tenant_budget.html.erb +73 -99
- data/app/views/ruby_llm/agents/dashboard/index.html.erb +228 -433
- data/app/views/ruby_llm/agents/executions/_execution.html.erb +1 -1
- data/app/views/ruby_llm/agents/executions/_filters.html.erb +4 -25
- data/app/views/ruby_llm/agents/executions/_list.html.erb +111 -152
- data/app/views/ruby_llm/agents/executions/index.html.erb +5 -7
- data/app/views/ruby_llm/agents/executions/show.html.erb +526 -1037
- data/app/views/ruby_llm/agents/shared/_agent_type_badge.html.erb +5 -21
- data/app/views/ruby_llm/agents/shared/_executions_table.html.erb +70 -191
- data/app/views/ruby_llm/agents/shared/_filter_dropdown.html.erb +16 -44
- data/app/views/ruby_llm/agents/shared/_select_dropdown.html.erb +12 -41
- data/app/views/ruby_llm/agents/shared/_status_badge.html.erb +11 -65
- data/app/views/ruby_llm/agents/shared/_tenant_filter.html.erb +6 -5
- data/app/views/ruby_llm/agents/system_config/show.html.erb +240 -351
- data/app/views/ruby_llm/agents/tenants/_form.html.erb +67 -77
- data/app/views/ruby_llm/agents/tenants/edit.html.erb +7 -9
- data/app/views/ruby_llm/agents/tenants/index.html.erb +100 -122
- data/app/views/ruby_llm/agents/tenants/show.html.erb +146 -336
- data/config/routes.rb +0 -13
- data/lib/generators/ruby_llm_agents/install_generator.rb +9 -14
- data/lib/generators/ruby_llm_agents/migrate_structure_generator.rb +2 -12
- data/lib/generators/ruby_llm_agents/restructure_generator.rb +0 -2
- data/lib/generators/ruby_llm_agents/templates/add_usage_counters_to_tenants_migration.rb.tt +37 -0
- data/lib/generators/ruby_llm_agents/templates/agent.rb.tt +1 -2
- data/lib/generators/ruby_llm_agents/templates/application_agent.rb.tt +1 -1
- data/lib/generators/ruby_llm_agents/templates/application_image_pipeline.rb.tt +0 -1
- data/lib/generators/ruby_llm_agents/templates/create_execution_details_migration.rb.tt +27 -0
- data/lib/generators/ruby_llm_agents/templates/create_tenants_migration.rb.tt +25 -0
- data/lib/generators/ruby_llm_agents/templates/image_pipeline.rb.tt +0 -1
- data/lib/generators/ruby_llm_agents/templates/initializer.rb.tt +9 -12
- data/lib/generators/ruby_llm_agents/templates/migration.rb.tt +40 -71
- data/lib/generators/ruby_llm_agents/templates/remove_agent_version_migration.rb.tt +13 -0
- data/lib/generators/ruby_llm_agents/templates/remove_workflow_columns_migration.rb.tt +19 -0
- data/lib/generators/ruby_llm_agents/templates/skills/AGENTS.md.tt +2 -4
- data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_PIPELINES.md.tt +0 -1
- data/lib/generators/ruby_llm_agents/templates/split_execution_details_migration.rb.tt +232 -0
- data/lib/generators/ruby_llm_agents/upgrade_generator.rb +58 -262
- data/lib/ruby_llm/agents/audio/speaker.rb +0 -1
- data/lib/ruby_llm/agents/audio/transcriber.rb +0 -1
- data/lib/ruby_llm/agents/base_agent.rb +52 -6
- data/lib/ruby_llm/agents/core/base/callbacks.rb +142 -0
- data/lib/ruby_llm/agents/core/base.rb +23 -55
- data/lib/ruby_llm/agents/core/configuration.rb +58 -117
- data/lib/ruby_llm/agents/core/errors.rb +0 -58
- data/lib/ruby_llm/agents/core/instrumentation.rb +157 -110
- data/lib/ruby_llm/agents/core/llm_tenant.rb +8 -7
- data/lib/ruby_llm/agents/core/version.rb +1 -1
- data/lib/ruby_llm/agents/dsl/base.rb +157 -17
- data/lib/ruby_llm/agents/dsl/caching.rb +33 -2
- data/lib/ruby_llm/agents/dsl/reliability.rb +148 -0
- data/lib/ruby_llm/agents/dsl.rb +1 -2
- data/lib/ruby_llm/agents/image/analyzer/execution.rb +1 -2
- data/lib/ruby_llm/agents/image/background_remover/execution.rb +1 -2
- data/lib/ruby_llm/agents/image/concerns/image_operation_dsl.rb +1 -13
- data/lib/ruby_llm/agents/image/concerns/image_operation_execution.rb +2 -2
- data/lib/ruby_llm/agents/image/editor/dsl.rb +0 -14
- data/lib/ruby_llm/agents/image/editor/execution.rb +1 -10
- data/lib/ruby_llm/agents/image/editor.rb +0 -1
- data/lib/ruby_llm/agents/image/generator.rb +0 -21
- data/lib/ruby_llm/agents/image/pipeline/dsl.rb +0 -13
- data/lib/ruby_llm/agents/image/pipeline/execution.rb +0 -1
- data/lib/ruby_llm/agents/image/transformer/dsl.rb +0 -13
- data/lib/ruby_llm/agents/image/transformer/execution.rb +1 -10
- data/lib/ruby_llm/agents/image/transformer.rb +0 -1
- data/lib/ruby_llm/agents/image/upscaler/execution.rb +1 -2
- data/lib/ruby_llm/agents/image/variator/execution.rb +1 -2
- data/lib/ruby_llm/agents/infrastructure/alert_manager.rb +78 -173
- data/lib/ruby_llm/agents/infrastructure/budget/budget_query.rb +66 -2
- data/lib/ruby_llm/agents/infrastructure/budget/spend_recorder.rb +0 -12
- data/lib/ruby_llm/agents/infrastructure/circuit_breaker.rb +10 -13
- data/lib/ruby_llm/agents/pipeline/context.rb +0 -1
- data/lib/ruby_llm/agents/pipeline/middleware/budget.rb +28 -4
- data/lib/ruby_llm/agents/pipeline/middleware/cache.rb +3 -10
- data/lib/ruby_llm/agents/pipeline/middleware/instrumentation.rb +88 -55
- data/lib/ruby_llm/agents/pipeline/middleware/tenant.rb +5 -41
- data/lib/ruby_llm/agents/rails/engine.rb +6 -6
- data/lib/ruby_llm/agents/results/base.rb +1 -49
- data/lib/ruby_llm/agents/text/embedder.rb +0 -1
- data/lib/ruby_llm/agents.rb +1 -9
- data/lib/tasks/ruby_llm_agents.rake +34 -0
- metadata +12 -81
- data/app/controllers/ruby_llm/agents/api_configurations_controller.rb +0 -214
- data/app/controllers/ruby_llm/agents/workflows_controller.rb +0 -544
- data/app/mailers/ruby_llm/agents/alert_mailer.rb +0 -84
- data/app/mailers/ruby_llm/agents/application_mailer.rb +0 -28
- data/app/models/ruby_llm/agents/api_configuration.rb +0 -386
- data/app/models/ruby_llm/agents/execution/workflow.rb +0 -170
- data/app/models/ruby_llm/agents/tenant/configurable.rb +0 -135
- data/app/views/ruby_llm/agents/agents/_agent.html.erb +0 -98
- data/app/views/ruby_llm/agents/agents/_version_comparison.html.erb +0 -186
- data/app/views/ruby_llm/agents/agents/_workflow.html.erb +0 -126
- data/app/views/ruby_llm/agents/alert_mailer/alert_notification.html.erb +0 -107
- data/app/views/ruby_llm/agents/alert_mailer/alert_notification.text.erb +0 -18
- data/app/views/ruby_llm/agents/api_configurations/_api_key_field.html.erb +0 -34
- data/app/views/ruby_llm/agents/api_configurations/_form.html.erb +0 -288
- data/app/views/ruby_llm/agents/api_configurations/edit.html.erb +0 -95
- data/app/views/ruby_llm/agents/api_configurations/edit_tenant.html.erb +0 -97
- data/app/views/ruby_llm/agents/api_configurations/show.html.erb +0 -214
- data/app/views/ruby_llm/agents/api_configurations/tenant.html.erb +0 -179
- data/app/views/ruby_llm/agents/dashboard/_agent_comparison.html.erb +0 -73
- data/app/views/ruby_llm/agents/dashboard/_alerts_feed.html.erb +0 -62
- data/app/views/ruby_llm/agents/dashboard/_breaker_strip.html.erb +0 -47
- data/app/views/ruby_llm/agents/dashboard/_budgets_bar.html.erb +0 -75
- data/app/views/ruby_llm/agents/dashboard/_model_comparison.html.erb +0 -56
- data/app/views/ruby_llm/agents/dashboard/_model_cost_breakdown.html.erb +0 -115
- data/app/views/ruby_llm/agents/dashboard/_now_strip.html.erb +0 -59
- data/app/views/ruby_llm/agents/dashboard/_top_errors.html.erb +0 -60
- data/app/views/ruby_llm/agents/executions/_workflow_summary.html.erb +0 -86
- data/app/views/ruby_llm/agents/executions/dry_run.html.erb +0 -149
- data/app/views/ruby_llm/agents/shared/_breadcrumbs.html.erb +0 -48
- data/app/views/ruby_llm/agents/shared/_nav_link.html.erb +0 -27
- data/app/views/ruby_llm/agents/shared/_stat_card.html.erb +0 -14
- data/app/views/ruby_llm/agents/shared/_workflow_type_badge.html.erb +0 -35
- data/app/views/ruby_llm/agents/workflows/_empty_state.html.erb +0 -22
- data/app/views/ruby_llm/agents/workflows/_step_performance.html.erb +0 -228
- data/app/views/ruby_llm/agents/workflows/_structure_dsl.html.erb +0 -539
- data/app/views/ruby_llm/agents/workflows/_structure_parallel.html.erb +0 -76
- data/app/views/ruby_llm/agents/workflows/_structure_pipeline.html.erb +0 -74
- data/app/views/ruby_llm/agents/workflows/_structure_router.html.erb +0 -108
- data/app/views/ruby_llm/agents/workflows/_workflow_diagram.html.erb +0 -920
- data/app/views/ruby_llm/agents/workflows/index.html.erb +0 -179
- data/app/views/ruby_llm/agents/workflows/show.html.erb +0 -467
- data/lib/generators/ruby_llm_agents/api_configuration_generator.rb +0 -100
- data/lib/generators/ruby_llm_agents/templates/add_workflow_migration.rb.tt +0 -38
- data/lib/generators/ruby_llm_agents/templates/application_workflow.rb.tt +0 -48
- data/lib/generators/ruby_llm_agents/templates/create_api_configurations_migration.rb.tt +0 -90
- data/lib/generators/ruby_llm_agents/templates/skills/WORKFLOWS.md.tt +0 -551
- data/lib/ruby_llm/agents/core/base/moderation_dsl.rb +0 -181
- data/lib/ruby_llm/agents/core/base/moderation_execution.rb +0 -274
- data/lib/ruby_llm/agents/core/resolved_config.rb +0 -348
- data/lib/ruby_llm/agents/image/generator/content_policy.rb +0 -95
- data/lib/ruby_llm/agents/infrastructure/redactor.rb +0 -130
- data/lib/ruby_llm/agents/results/moderation_result.rb +0 -158
- data/lib/ruby_llm/agents/text/moderator.rb +0 -237
- data/lib/ruby_llm/agents/workflow/approval.rb +0 -205
- data/lib/ruby_llm/agents/workflow/approval_store.rb +0 -179
- data/lib/ruby_llm/agents/workflow/async.rb +0 -220
- data/lib/ruby_llm/agents/workflow/async_executor.rb +0 -156
- data/lib/ruby_llm/agents/workflow/dsl/executor.rb +0 -467
- data/lib/ruby_llm/agents/workflow/dsl/input_schema.rb +0 -244
- data/lib/ruby_llm/agents/workflow/dsl/iteration_executor.rb +0 -289
- data/lib/ruby_llm/agents/workflow/dsl/parallel_group.rb +0 -107
- data/lib/ruby_llm/agents/workflow/dsl/route_builder.rb +0 -150
- data/lib/ruby_llm/agents/workflow/dsl/schedule_helpers.rb +0 -187
- data/lib/ruby_llm/agents/workflow/dsl/step_config.rb +0 -352
- data/lib/ruby_llm/agents/workflow/dsl/step_executor.rb +0 -415
- data/lib/ruby_llm/agents/workflow/dsl/wait_config.rb +0 -257
- data/lib/ruby_llm/agents/workflow/dsl/wait_executor.rb +0 -317
- data/lib/ruby_llm/agents/workflow/dsl.rb +0 -576
- data/lib/ruby_llm/agents/workflow/instrumentation.rb +0 -249
- data/lib/ruby_llm/agents/workflow/notifiers/base.rb +0 -117
- data/lib/ruby_llm/agents/workflow/notifiers/email.rb +0 -117
- data/lib/ruby_llm/agents/workflow/notifiers/slack.rb +0 -180
- data/lib/ruby_llm/agents/workflow/notifiers/webhook.rb +0 -121
- data/lib/ruby_llm/agents/workflow/notifiers.rb +0 -70
- data/lib/ruby_llm/agents/workflow/orchestrator.rb +0 -416
- data/lib/ruby_llm/agents/workflow/result.rb +0 -592
- data/lib/ruby_llm/agents/workflow/thread_pool.rb +0 -185
- data/lib/ruby_llm/agents/workflow/throttle_manager.rb +0 -206
- 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
|