ruby_llm-agents 1.3.3 → 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 +528 -989
- 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/attempt_tracker.rb +1 -0
- 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/infrastructure/reliability.rb +37 -2
- 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,7 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative "../concerns/image_operation_dsl"
|
|
4
|
-
require_relative "../generator/content_policy"
|
|
5
4
|
|
|
6
5
|
module RubyLLM
|
|
7
6
|
module Agents
|
|
@@ -70,18 +69,6 @@ module RubyLLM
|
|
|
70
69
|
end
|
|
71
70
|
end
|
|
72
71
|
|
|
73
|
-
# Set or get the content policy level
|
|
74
|
-
#
|
|
75
|
-
# @param level [Symbol, nil] Policy level (:none, :standard, :moderate, :strict)
|
|
76
|
-
# @return [Symbol] The content policy level
|
|
77
|
-
def content_policy(level = nil)
|
|
78
|
-
if level
|
|
79
|
-
@content_policy = level
|
|
80
|
-
else
|
|
81
|
-
@content_policy || inherited_or_default(:content_policy, :standard)
|
|
82
|
-
end
|
|
83
|
-
end
|
|
84
|
-
|
|
85
72
|
# Set a prompt template (use {prompt} as placeholder)
|
|
86
73
|
#
|
|
87
74
|
# @param value [String, nil] Template string
|
|
@@ -23,7 +23,6 @@ module RubyLLM
|
|
|
23
23
|
resolve_tenant_context!
|
|
24
24
|
check_budget! if budget_tracking_enabled?
|
|
25
25
|
validate_inputs!
|
|
26
|
-
validate_content_policy!
|
|
27
26
|
|
|
28
27
|
# Check cache
|
|
29
28
|
cached = check_cache(ImageTransformResult) if cache_enabled?
|
|
@@ -75,13 +74,6 @@ module RubyLLM
|
|
|
75
74
|
end
|
|
76
75
|
end
|
|
77
76
|
|
|
78
|
-
def validate_content_policy!
|
|
79
|
-
policy = self.class.content_policy
|
|
80
|
-
return if policy == :none || policy == :standard
|
|
81
|
-
|
|
82
|
-
ImageGenerator::ContentPolicy.validate!(prompt, policy)
|
|
83
|
-
end
|
|
84
|
-
|
|
85
77
|
def transform_images
|
|
86
78
|
count = resolve_count
|
|
87
79
|
|
|
@@ -188,7 +180,6 @@ module RubyLLM
|
|
|
188
180
|
[
|
|
189
181
|
"image_transformer",
|
|
190
182
|
self.class.name,
|
|
191
|
-
self.class.version,
|
|
192
183
|
resolve_model,
|
|
193
184
|
resolve_size,
|
|
194
185
|
resolve_strength.to_s,
|
|
@@ -209,7 +200,7 @@ module RubyLLM
|
|
|
209
200
|
end
|
|
210
201
|
end
|
|
211
202
|
|
|
212
|
-
def
|
|
203
|
+
def build_metadata(result)
|
|
213
204
|
{
|
|
214
205
|
count: result.count,
|
|
215
206
|
size: result.size,
|
|
@@ -57,7 +57,6 @@ module RubyLLM
|
|
|
57
57
|
subclass.instance_variable_set(:@version, @version)
|
|
58
58
|
subclass.instance_variable_set(:@description, @description)
|
|
59
59
|
subclass.instance_variable_set(:@cache_ttl, @cache_ttl)
|
|
60
|
-
subclass.instance_variable_set(:@content_policy, @content_policy)
|
|
61
60
|
subclass.instance_variable_set(:@template_string, @template_string)
|
|
62
61
|
subclass.instance_variable_set(:@negative_prompt, @negative_prompt)
|
|
63
62
|
subclass.instance_variable_set(:@guidance_scale, @guidance_scale)
|
|
@@ -186,7 +186,6 @@ module RubyLLM
|
|
|
186
186
|
[
|
|
187
187
|
"image_upscaler",
|
|
188
188
|
self.class.name,
|
|
189
|
-
self.class.version,
|
|
190
189
|
resolve_model,
|
|
191
190
|
resolve_scale.to_s,
|
|
192
191
|
resolve_face_enhance.to_s,
|
|
@@ -206,7 +205,7 @@ module RubyLLM
|
|
|
206
205
|
end
|
|
207
206
|
end
|
|
208
207
|
|
|
209
|
-
def
|
|
208
|
+
def build_metadata(result)
|
|
210
209
|
{
|
|
211
210
|
scale: result.scale,
|
|
212
211
|
output_size: result.output_size,
|
|
@@ -156,7 +156,6 @@ module RubyLLM
|
|
|
156
156
|
[
|
|
157
157
|
"image_variator",
|
|
158
158
|
self.class.name,
|
|
159
|
-
self.class.version,
|
|
160
159
|
resolve_model,
|
|
161
160
|
resolve_size,
|
|
162
161
|
resolve_variation_strength.to_s,
|
|
@@ -176,7 +175,7 @@ module RubyLLM
|
|
|
176
175
|
end
|
|
177
176
|
end
|
|
178
177
|
|
|
179
|
-
def
|
|
178
|
+
def build_metadata(result)
|
|
180
179
|
{
|
|
181
180
|
count: result.count,
|
|
182
181
|
size: result.size,
|
|
@@ -1,127 +1,85 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "net/http"
|
|
4
|
-
require "uri"
|
|
5
|
-
require "json"
|
|
6
|
-
|
|
7
3
|
module RubyLLM
|
|
8
4
|
module Agents
|
|
9
5
|
# Alert notification dispatcher for governance events
|
|
10
6
|
#
|
|
11
|
-
# Sends notifications
|
|
7
|
+
# Sends notifications via user-provided handler and ActiveSupport::Notifications
|
|
12
8
|
# when important events occur like budget exceedance or circuit breaker activation.
|
|
13
9
|
#
|
|
14
|
-
# @example
|
|
15
|
-
#
|
|
10
|
+
# @example Configure an alert handler
|
|
11
|
+
# RubyLLM::Agents.configure do |config|
|
|
12
|
+
# config.on_alert = ->(event, payload) {
|
|
13
|
+
# case event
|
|
14
|
+
# when :budget_hard_cap
|
|
15
|
+
# Slack::Notifier.new(ENV["SLACK_WEBHOOK"]).ping("Budget exceeded")
|
|
16
|
+
# end
|
|
17
|
+
# }
|
|
18
|
+
# end
|
|
19
|
+
#
|
|
20
|
+
# @example Subscribe via ActiveSupport::Notifications
|
|
21
|
+
# ActiveSupport::Notifications.subscribe(/^ruby_llm_agents\.alert\./) do |name, _, _, _, payload|
|
|
22
|
+
# event = name.sub("ruby_llm_agents.alert.", "").to_sym
|
|
23
|
+
# MyAlertService.handle(event, payload)
|
|
24
|
+
# end
|
|
16
25
|
#
|
|
17
|
-
# @see RubyLLM::Agents::Configuration
|
|
26
|
+
# @see RubyLLM::Agents::Configuration#on_alert
|
|
18
27
|
# @api public
|
|
19
28
|
module AlertManager
|
|
20
29
|
class << self
|
|
21
|
-
# Sends a notification to
|
|
30
|
+
# Sends a notification to the configured handler and emits AS::N
|
|
22
31
|
#
|
|
23
32
|
# @param event [Symbol] The event type (e.g., :budget_soft_cap, :breaker_open)
|
|
24
33
|
# @param payload [Hash] Event-specific data
|
|
25
34
|
# @return [void]
|
|
26
35
|
def notify(event, payload)
|
|
27
|
-
|
|
28
|
-
return unless config.alerts_enabled?
|
|
29
|
-
return unless config.alert_events.include?(event)
|
|
30
|
-
|
|
31
|
-
alerts = config.alerts
|
|
32
|
-
full_payload = payload.merge(event: event)
|
|
33
|
-
|
|
34
|
-
# Send to Slack
|
|
35
|
-
if alerts[:slack_webhook_url].present?
|
|
36
|
-
send_slack_alert(alerts[:slack_webhook_url], event, full_payload)
|
|
37
|
-
end
|
|
38
|
-
|
|
39
|
-
# Send to generic webhook
|
|
40
|
-
if alerts[:webhook_url].present?
|
|
41
|
-
send_webhook_alert(alerts[:webhook_url], full_payload)
|
|
42
|
-
end
|
|
43
|
-
|
|
44
|
-
# Call custom proc
|
|
45
|
-
if alerts[:custom].respond_to?(:call)
|
|
46
|
-
call_custom_alert(alerts[:custom], event, full_payload)
|
|
47
|
-
end
|
|
36
|
+
full_payload = build_payload(event, payload)
|
|
48
37
|
|
|
49
|
-
#
|
|
50
|
-
|
|
51
|
-
email_events = alerts[:email_events] || config.alert_events
|
|
52
|
-
if email_events.include?(event)
|
|
53
|
-
send_email_alerts(event, full_payload, alerts[:email_recipients])
|
|
54
|
-
end
|
|
55
|
-
end
|
|
38
|
+
# Call user-provided handler (if set)
|
|
39
|
+
call_handler(event, full_payload)
|
|
56
40
|
|
|
57
|
-
#
|
|
41
|
+
# Always emit ActiveSupport::Notification
|
|
58
42
|
emit_notification(event, full_payload)
|
|
43
|
+
|
|
44
|
+
# Store in cache for dashboard display
|
|
45
|
+
store_for_dashboard(event, full_payload)
|
|
59
46
|
rescue StandardError => e
|
|
60
|
-
|
|
61
|
-
Rails.logger.error("[RubyLLM::Agents::AlertManager] Failed to send alert: #{e.message}")
|
|
47
|
+
Rails.logger.error("[RubyLLM::Agents::AlertManager] Failed: #{e.message}")
|
|
62
48
|
end
|
|
63
49
|
|
|
64
50
|
private
|
|
65
51
|
|
|
66
|
-
#
|
|
52
|
+
# Builds the full payload with standard fields
|
|
67
53
|
#
|
|
68
|
-
# @param webhook_url [String] The Slack webhook URL
|
|
69
54
|
# @param event [Symbol] The event type
|
|
70
|
-
# @param payload [Hash] The payload
|
|
71
|
-
# @return [
|
|
72
|
-
def
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
end
|
|
79
|
-
|
|
80
|
-
# Sends a generic webhook alert
|
|
81
|
-
#
|
|
82
|
-
# @param webhook_url [String] The webhook URL
|
|
83
|
-
# @param payload [Hash] The payload
|
|
84
|
-
# @return [void]
|
|
85
|
-
def send_webhook_alert(webhook_url, payload)
|
|
86
|
-
post_json(webhook_url, payload)
|
|
87
|
-
rescue StandardError => e
|
|
88
|
-
Rails.logger.warn("[RubyLLM::Agents::AlertManager] Webhook alert failed: #{e.message}")
|
|
55
|
+
# @param payload [Hash] The original payload
|
|
56
|
+
# @return [Hash] Payload with event, timestamp, and tenant_id added
|
|
57
|
+
def build_payload(event, payload)
|
|
58
|
+
payload.merge(
|
|
59
|
+
event: event,
|
|
60
|
+
timestamp: Time.current,
|
|
61
|
+
tenant_id: RubyLLM::Agents.configuration.current_tenant_id
|
|
62
|
+
)
|
|
89
63
|
end
|
|
90
64
|
|
|
91
|
-
# Calls
|
|
65
|
+
# Calls the user-provided alert handler
|
|
92
66
|
#
|
|
93
|
-
# @param custom_proc [Proc] The custom handler
|
|
94
67
|
# @param event [Symbol] The event type
|
|
95
|
-
# @param payload [Hash] The payload
|
|
68
|
+
# @param payload [Hash] The full payload
|
|
96
69
|
# @return [void]
|
|
97
|
-
def
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
Rails.logger.warn("[RubyLLM::Agents::AlertManager] Custom alert failed: #{e.message}")
|
|
101
|
-
end
|
|
70
|
+
def call_handler(event, payload)
|
|
71
|
+
handler = RubyLLM::Agents.configuration.on_alert
|
|
72
|
+
return unless handler.respond_to?(:call)
|
|
102
73
|
|
|
103
|
-
|
|
104
|
-
#
|
|
105
|
-
# @param event [Symbol] The event type
|
|
106
|
-
# @param payload [Hash] The payload
|
|
107
|
-
# @param recipients [Array<String>] Email addresses
|
|
108
|
-
# @return [void]
|
|
109
|
-
def send_email_alerts(event, payload, recipients)
|
|
110
|
-
Array(recipients).each do |recipient|
|
|
111
|
-
AlertMailer.alert_notification(
|
|
112
|
-
event: event,
|
|
113
|
-
payload: payload,
|
|
114
|
-
recipient: recipient
|
|
115
|
-
).deliver_later
|
|
116
|
-
end
|
|
74
|
+
handler.call(event, payload)
|
|
117
75
|
rescue StandardError => e
|
|
118
|
-
Rails.logger.warn("[RubyLLM::Agents::AlertManager]
|
|
76
|
+
Rails.logger.warn("[RubyLLM::Agents::AlertManager] Handler failed: #{e.message}")
|
|
119
77
|
end
|
|
120
78
|
|
|
121
79
|
# Emits an ActiveSupport::Notification
|
|
122
80
|
#
|
|
123
81
|
# @param event [Symbol] The event type
|
|
124
|
-
# @param payload [Hash] The payload
|
|
82
|
+
# @param payload [Hash] The full payload
|
|
125
83
|
# @return [void]
|
|
126
84
|
def emit_notification(event, payload)
|
|
127
85
|
ActiveSupport::Notifications.instrument("ruby_llm_agents.alert.#{event}", payload)
|
|
@@ -129,102 +87,49 @@ module RubyLLM
|
|
|
129
87
|
# Ignore notification failures
|
|
130
88
|
end
|
|
131
89
|
|
|
132
|
-
#
|
|
133
|
-
#
|
|
134
|
-
# @param event [Symbol] The event type
|
|
135
|
-
# @param payload [Hash] The payload
|
|
136
|
-
# @return [Hash] Slack message payload
|
|
137
|
-
def format_slack_message(event, payload)
|
|
138
|
-
emoji = event_emoji(event)
|
|
139
|
-
title = event_title(event)
|
|
140
|
-
color = event_color(event)
|
|
141
|
-
|
|
142
|
-
fields = payload.except(:event).map do |key, value|
|
|
143
|
-
{
|
|
144
|
-
title: key.to_s.titleize,
|
|
145
|
-
value: value.to_s,
|
|
146
|
-
short: true
|
|
147
|
-
}
|
|
148
|
-
end
|
|
149
|
-
|
|
150
|
-
{
|
|
151
|
-
attachments: [
|
|
152
|
-
{
|
|
153
|
-
fallback: "#{title}: #{payload.except(:event).to_json}",
|
|
154
|
-
color: color,
|
|
155
|
-
pretext: "#{emoji} *RubyLLM::Agents Alert*",
|
|
156
|
-
title: title,
|
|
157
|
-
fields: fields,
|
|
158
|
-
footer: "RubyLLM::Agents",
|
|
159
|
-
ts: Time.current.to_i
|
|
160
|
-
}
|
|
161
|
-
]
|
|
162
|
-
}
|
|
163
|
-
end
|
|
164
|
-
|
|
165
|
-
# Returns emoji for event type
|
|
166
|
-
#
|
|
167
|
-
# @param event [Symbol] The event type
|
|
168
|
-
# @return [String] Emoji
|
|
169
|
-
def event_emoji(event)
|
|
170
|
-
case event
|
|
171
|
-
when :budget_soft_cap then ":warning:"
|
|
172
|
-
when :budget_hard_cap then ":no_entry:"
|
|
173
|
-
when :breaker_open then ":rotating_light:"
|
|
174
|
-
when :agent_anomaly then ":mag:"
|
|
175
|
-
else ":bell:"
|
|
176
|
-
end
|
|
177
|
-
end
|
|
178
|
-
|
|
179
|
-
# Returns title for event type
|
|
90
|
+
# Stores the alert in cache for dashboard display
|
|
180
91
|
#
|
|
181
92
|
# @param event [Symbol] The event type
|
|
182
|
-
# @
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
93
|
+
# @param payload [Hash] The full payload
|
|
94
|
+
# @return [void]
|
|
95
|
+
def store_for_dashboard(event, payload)
|
|
96
|
+
cache = RubyLLM::Agents.configuration.cache_store
|
|
97
|
+
key = "ruby_llm_agents:alerts:recent"
|
|
98
|
+
|
|
99
|
+
alerts = cache.read(key) || []
|
|
100
|
+
alerts.unshift(
|
|
101
|
+
type: event,
|
|
102
|
+
message: format_message(event, payload),
|
|
103
|
+
agent_type: payload[:agent_type],
|
|
104
|
+
timestamp: payload[:timestamp]
|
|
105
|
+
)
|
|
106
|
+
alerts = alerts.first(50)
|
|
107
|
+
|
|
108
|
+
cache.write(key, alerts, expires_in: 24.hours)
|
|
109
|
+
rescue StandardError
|
|
110
|
+
# Ignore cache failures
|
|
191
111
|
end
|
|
192
112
|
|
|
193
|
-
#
|
|
113
|
+
# Formats a human-readable message for the event
|
|
194
114
|
#
|
|
195
115
|
# @param event [Symbol] The event type
|
|
196
|
-
# @
|
|
197
|
-
|
|
116
|
+
# @param payload [Hash] The full payload
|
|
117
|
+
# @return [String] Human-readable message
|
|
118
|
+
def format_message(event, payload)
|
|
198
119
|
case event
|
|
199
|
-
when :budget_soft_cap
|
|
200
|
-
|
|
201
|
-
when :
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
# @return [Net::HTTPResponse]
|
|
212
|
-
def post_json(url, payload)
|
|
213
|
-
uri = URI.parse(url)
|
|
214
|
-
http = Net::HTTP.new(uri.host, uri.port)
|
|
215
|
-
http.use_ssl = (uri.scheme == "https")
|
|
216
|
-
http.open_timeout = 5
|
|
217
|
-
http.read_timeout = 10
|
|
218
|
-
|
|
219
|
-
request = Net::HTTP::Post.new(uri.request_uri)
|
|
220
|
-
request["Content-Type"] = "application/json"
|
|
221
|
-
request.body = payload.to_json
|
|
222
|
-
|
|
223
|
-
response = http.request(request)
|
|
224
|
-
unless response.is_a?(Net::HTTPSuccess)
|
|
225
|
-
Rails.logger.warn("[RubyLLM::Agents::AlertManager] Webhook returned #{response.code}: #{response.body}")
|
|
120
|
+
when :budget_soft_cap
|
|
121
|
+
"Budget soft cap reached: $#{payload[:total_cost]&.round(2)} / $#{payload[:limit]&.round(2)}"
|
|
122
|
+
when :budget_hard_cap
|
|
123
|
+
"Budget hard cap exceeded: $#{payload[:total_cost]&.round(2)} / $#{payload[:limit]&.round(2)}"
|
|
124
|
+
when :breaker_open
|
|
125
|
+
"Circuit breaker opened for #{payload[:agent_type]}"
|
|
126
|
+
when :breaker_closed
|
|
127
|
+
"Circuit breaker closed for #{payload[:agent_type]}"
|
|
128
|
+
when :agent_anomaly
|
|
129
|
+
"Anomaly detected: #{payload[:threshold_type]} threshold exceeded"
|
|
130
|
+
else
|
|
131
|
+
event.to_s.humanize
|
|
226
132
|
end
|
|
227
|
-
response
|
|
228
133
|
end
|
|
229
134
|
end
|
|
230
135
|
end
|
|
@@ -21,7 +21,16 @@ module RubyLLM
|
|
|
21
21
|
# @return [Float] Current spend in USD
|
|
22
22
|
def current_spend(scope, period, agent_type: nil, tenant_id: nil)
|
|
23
23
|
key = SpendRecorder.budget_cache_key(scope, period, agent_type: agent_type, tenant_id: tenant_id)
|
|
24
|
-
|
|
24
|
+
cached = BudgetQuery.cache_read(key)
|
|
25
|
+
return cached.to_f if cached.present?
|
|
26
|
+
|
|
27
|
+
# Cache miss — rebuild from executions table for global scope
|
|
28
|
+
if scope == :global && tenant_id.nil?
|
|
29
|
+
total = current_global_spend(period)
|
|
30
|
+
return total.to_f
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
0.to_f
|
|
25
34
|
end
|
|
26
35
|
|
|
27
36
|
# Returns the current token usage for a period (global only)
|
|
@@ -31,9 +40,64 @@ module RubyLLM
|
|
|
31
40
|
# @return [Integer] Current token usage
|
|
32
41
|
def current_tokens(period, tenant_id: nil)
|
|
33
42
|
key = SpendRecorder.token_cache_key(period, tenant_id: tenant_id)
|
|
34
|
-
|
|
43
|
+
cached = BudgetQuery.cache_read(key)
|
|
44
|
+
return cached.to_i if cached.present?
|
|
45
|
+
|
|
46
|
+
# Cache miss — rebuild from executions table
|
|
47
|
+
if tenant_id.nil?
|
|
48
|
+
total = current_global_tokens(period)
|
|
49
|
+
return total.to_i
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
0
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Rebuilds global spend from executions table on cache miss
|
|
56
|
+
#
|
|
57
|
+
# @param period [Symbol] :daily or :monthly
|
|
58
|
+
# @return [Float] Total spend in USD
|
|
59
|
+
def current_global_spend(period)
|
|
60
|
+
total = RubyLLM::Agents::Execution
|
|
61
|
+
.where("created_at >= ?", period_start(period))
|
|
62
|
+
.where(tenant_id: nil)
|
|
63
|
+
.sum(:total_cost)
|
|
64
|
+
key = SpendRecorder.budget_cache_key(:global, period)
|
|
65
|
+
BudgetQuery.cache_write(key, total, expires_in: period_ttl(period))
|
|
66
|
+
total
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Rebuilds global token usage from executions table on cache miss
|
|
70
|
+
#
|
|
71
|
+
# @param period [Symbol] :daily or :monthly
|
|
72
|
+
# @return [Integer] Total tokens used
|
|
73
|
+
def current_global_tokens(period)
|
|
74
|
+
total = RubyLLM::Agents::Execution
|
|
75
|
+
.where("created_at >= ?", period_start(period))
|
|
76
|
+
.where(tenant_id: nil)
|
|
77
|
+
.sum(:total_tokens)
|
|
78
|
+
key = SpendRecorder.token_cache_key(period)
|
|
79
|
+
BudgetQuery.cache_write(key, total, expires_in: period_ttl(period))
|
|
80
|
+
total
|
|
35
81
|
end
|
|
36
82
|
|
|
83
|
+
private
|
|
84
|
+
|
|
85
|
+
def period_start(period)
|
|
86
|
+
case period
|
|
87
|
+
when :daily then Date.current.beginning_of_day
|
|
88
|
+
when :monthly then Date.current.beginning_of_month.beginning_of_day
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def period_ttl(period)
|
|
93
|
+
case period
|
|
94
|
+
when :daily then 1.day
|
|
95
|
+
when :monthly then 31.days
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
public
|
|
100
|
+
|
|
37
101
|
# Returns the remaining budget for a scope and period
|
|
38
102
|
#
|
|
39
103
|
# @param scope [Symbol] :global or :agent
|
|
@@ -154,10 +154,6 @@ module RubyLLM
|
|
|
154
154
|
# @param budget_config [Hash] Budget configuration
|
|
155
155
|
# @return [void]
|
|
156
156
|
def check_soft_cap_alerts(agent_type, tenant_id, budget_config)
|
|
157
|
-
config = RubyLLM::Agents.configuration
|
|
158
|
-
return unless config.alerts_enabled?
|
|
159
|
-
return unless config.alert_events.include?(:budget_soft_cap) || config.alert_events.include?(:budget_hard_cap)
|
|
160
|
-
|
|
161
157
|
# Check global daily
|
|
162
158
|
check_budget_alert(:global_daily, budget_config[:global_daily],
|
|
163
159
|
BudgetQuery.current_spend(:global, :daily, tenant_id: tenant_id),
|
|
@@ -199,8 +195,6 @@ module RubyLLM
|
|
|
199
195
|
return if current <= limit
|
|
200
196
|
|
|
201
197
|
event = budget_config[:enforcement] == :hard ? :budget_hard_cap : :budget_soft_cap
|
|
202
|
-
config = RubyLLM::Agents.configuration
|
|
203
|
-
return unless config.alert_events.include?(event)
|
|
204
198
|
|
|
205
199
|
# Prevent duplicate alerts by using a cache key (include tenant for isolation)
|
|
206
200
|
key = alert_cache_key("budget_alert", scope, tenant_id)
|
|
@@ -225,10 +219,6 @@ module RubyLLM
|
|
|
225
219
|
# @param budget_config [Hash] Budget configuration
|
|
226
220
|
# @return [void]
|
|
227
221
|
def check_soft_token_alerts(agent_type, tenant_id, budget_config)
|
|
228
|
-
config = RubyLLM::Agents.configuration
|
|
229
|
-
return unless config.alerts_enabled?
|
|
230
|
-
return unless config.alert_events.include?(:token_soft_cap) || config.alert_events.include?(:token_hard_cap)
|
|
231
|
-
|
|
232
222
|
# Check global daily tokens
|
|
233
223
|
check_token_alert(:global_daily_tokens, budget_config[:global_daily_tokens],
|
|
234
224
|
BudgetQuery.current_tokens(:daily, tenant_id: tenant_id),
|
|
@@ -254,8 +244,6 @@ module RubyLLM
|
|
|
254
244
|
return if current <= limit
|
|
255
245
|
|
|
256
246
|
event = budget_config[:enforcement] == :hard ? :token_hard_cap : :token_soft_cap
|
|
257
|
-
config = RubyLLM::Agents.configuration
|
|
258
|
-
return unless config.alert_events.include?(event)
|
|
259
247
|
|
|
260
248
|
# Prevent duplicate alerts
|
|
261
249
|
key = alert_cache_key("token_alert", scope, tenant_id)
|
|
@@ -174,19 +174,16 @@ module RubyLLM
|
|
|
174
174
|
def open_breaker!
|
|
175
175
|
cache_write(open_key, Time.current.to_s, expires_in: cooldown_seconds)
|
|
176
176
|
|
|
177
|
-
# Fire alert
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
timestamp: Time.current.iso8601
|
|
188
|
-
})
|
|
189
|
-
end
|
|
177
|
+
# Fire alert
|
|
178
|
+
AlertManager.notify(:breaker_open, {
|
|
179
|
+
agent_type: agent_type,
|
|
180
|
+
model_id: model_id,
|
|
181
|
+
tenant_id: tenant_id,
|
|
182
|
+
errors: errors_threshold,
|
|
183
|
+
within: window_seconds,
|
|
184
|
+
cooldown: cooldown_seconds,
|
|
185
|
+
timestamp: Time.current.iso8601
|
|
186
|
+
})
|
|
190
187
|
end
|
|
191
188
|
|
|
192
189
|
# Returns the cache key for the failure counter
|
|
@@ -88,7 +88,7 @@ module RubyLLM
|
|
|
88
88
|
#
|
|
89
89
|
# @api public
|
|
90
90
|
class AllModelsExhaustedError < Error
|
|
91
|
-
attr_reader :models_tried, :last_error, :attempts
|
|
91
|
+
attr_reader :models_tried, :last_error, :attempts, :errors
|
|
92
92
|
|
|
93
93
|
# @param models_tried [Array<String>] List of models that were attempted
|
|
94
94
|
# @param last_error [Exception] The last error that occurred
|
|
@@ -97,7 +97,42 @@ module RubyLLM
|
|
|
97
97
|
@models_tried = models_tried
|
|
98
98
|
@last_error = last_error
|
|
99
99
|
@attempts = attempts
|
|
100
|
-
|
|
100
|
+
@errors = build_errors
|
|
101
|
+
super(build_message)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
private
|
|
105
|
+
|
|
106
|
+
def build_errors
|
|
107
|
+
return [] unless @attempts&.any?
|
|
108
|
+
|
|
109
|
+
@attempts.filter_map do |attempt|
|
|
110
|
+
if attempt["short_circuited"]
|
|
111
|
+
{ model: attempt["model_id"], error_class: "CircuitBreakerOpen", error_message: "circuit breaker open",
|
|
112
|
+
error_backtrace: nil }
|
|
113
|
+
elsif attempt["error_class"]
|
|
114
|
+
{ model: attempt["model_id"], error_class: attempt["error_class"], error_message: attempt["error_message"],
|
|
115
|
+
error_backtrace: attempt["error_backtrace"] }
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def build_message
|
|
121
|
+
parts = ["All models exhausted:"]
|
|
122
|
+
if @attempts&.any?
|
|
123
|
+
@attempts.each do |attempt|
|
|
124
|
+
model = attempt["model_id"]
|
|
125
|
+
if attempt["short_circuited"]
|
|
126
|
+
parts << " #{model}: circuit breaker open"
|
|
127
|
+
elsif attempt["error_class"]
|
|
128
|
+
parts << " #{model}: #{attempt['error_class']} - #{attempt['error_message']}"
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
else
|
|
132
|
+
parts << " Models: #{@models_tried.join(', ')}"
|
|
133
|
+
parts << " Last error: #{@last_error.message}"
|
|
134
|
+
end
|
|
135
|
+
parts.join("\n")
|
|
101
136
|
end
|
|
102
137
|
end
|
|
103
138
|
|