ruby_llm-agents 1.3.4 → 2.1.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 +112 -336
- 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 +52 -12
- 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 +89 -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 +13 -17
- 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 +33 -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 +77 -259
- 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 +54 -23
- 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 +97 -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/infrastructure/execution_logger_job.rb +8 -0
- 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 +14 -83
- 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,544 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module RubyLLM
|
|
4
|
-
module Agents
|
|
5
|
-
# Controller for viewing workflow details and per-workflow analytics
|
|
6
|
-
#
|
|
7
|
-
# Provides detailed views for individual workflows including structure
|
|
8
|
-
# visualization, per-step/branch performance, route distribution,
|
|
9
|
-
# and execution history.
|
|
10
|
-
#
|
|
11
|
-
# @see AgentRegistry For workflow discovery
|
|
12
|
-
# @see Paginatable For pagination implementation
|
|
13
|
-
# @see Filterable For filter parsing and validation
|
|
14
|
-
# @api private
|
|
15
|
-
class WorkflowsController < ApplicationController
|
|
16
|
-
include Paginatable
|
|
17
|
-
include Filterable
|
|
18
|
-
|
|
19
|
-
# Lists all registered workflows with their details
|
|
20
|
-
#
|
|
21
|
-
# Uses AgentRegistry to discover workflows from both file system
|
|
22
|
-
# and execution history. Separates workflows by type for sub-tabs.
|
|
23
|
-
# Supports sorting by various columns.
|
|
24
|
-
#
|
|
25
|
-
# @return [void]
|
|
26
|
-
def index
|
|
27
|
-
all_items = AgentRegistry.all_with_details
|
|
28
|
-
@workflows = all_items.select { |a| a[:is_workflow] }
|
|
29
|
-
@sort_params = parse_sort_params
|
|
30
|
-
@workflows = sort_workflows(@workflows)
|
|
31
|
-
rescue StandardError => e
|
|
32
|
-
Rails.logger.error("[RubyLLM::Agents] Error loading workflows: #{e.message}")
|
|
33
|
-
@workflows = []
|
|
34
|
-
flash.now[:alert] = "Error loading workflows list"
|
|
35
|
-
end
|
|
36
|
-
|
|
37
|
-
# Shows detailed view for a specific workflow
|
|
38
|
-
#
|
|
39
|
-
# Loads workflow configuration (if class exists), statistics,
|
|
40
|
-
# filtered executions, chart data, and step-level analytics.
|
|
41
|
-
# Works for both active workflows and deleted workflows with history.
|
|
42
|
-
#
|
|
43
|
-
# @return [void]
|
|
44
|
-
def show
|
|
45
|
-
@workflow_type = CGI.unescape(params[:id])
|
|
46
|
-
@workflow_class = AgentRegistry.find(@workflow_type)
|
|
47
|
-
@workflow_active = @workflow_class.present?
|
|
48
|
-
|
|
49
|
-
# Determine workflow type from class or execution history
|
|
50
|
-
@workflow_type_kind = detect_workflow_type_kind
|
|
51
|
-
|
|
52
|
-
load_workflow_stats
|
|
53
|
-
load_filter_options
|
|
54
|
-
load_filtered_executions
|
|
55
|
-
load_chart_data
|
|
56
|
-
load_step_stats
|
|
57
|
-
|
|
58
|
-
if @workflow_class
|
|
59
|
-
load_workflow_config
|
|
60
|
-
end
|
|
61
|
-
rescue StandardError => e
|
|
62
|
-
Rails.logger.error("[RubyLLM::Agents] Error loading workflow #{@workflow_type}: #{e.message}")
|
|
63
|
-
redirect_to ruby_llm_agents.workflows_path, alert: "Error loading workflow details"
|
|
64
|
-
end
|
|
65
|
-
|
|
66
|
-
private
|
|
67
|
-
|
|
68
|
-
# Detects the workflow type kind
|
|
69
|
-
#
|
|
70
|
-
# All workflows now use the DSL and return "workflow" type.
|
|
71
|
-
#
|
|
72
|
-
# @return [String, nil] The workflow type kind
|
|
73
|
-
def detect_workflow_type_kind
|
|
74
|
-
if @workflow_class
|
|
75
|
-
if @workflow_class.respond_to?(:step_configs) && @workflow_class.step_configs.any?
|
|
76
|
-
"workflow"
|
|
77
|
-
end
|
|
78
|
-
else
|
|
79
|
-
# Fallback to execution history
|
|
80
|
-
Execution.by_agent(@workflow_type)
|
|
81
|
-
.where.not(workflow_type: nil)
|
|
82
|
-
.pluck(:workflow_type)
|
|
83
|
-
.first
|
|
84
|
-
end
|
|
85
|
-
end
|
|
86
|
-
|
|
87
|
-
# Loads all-time and today's statistics for the workflow
|
|
88
|
-
#
|
|
89
|
-
# @return [void]
|
|
90
|
-
def load_workflow_stats
|
|
91
|
-
@stats = Execution.stats_for(@workflow_type, period: :all_time)
|
|
92
|
-
@stats_today = Execution.stats_for(@workflow_type, period: :today)
|
|
93
|
-
|
|
94
|
-
# Additional stats for new schema fields
|
|
95
|
-
workflow_scope = Execution.by_agent(@workflow_type)
|
|
96
|
-
@cache_hit_rate = workflow_scope.cache_hit_rate
|
|
97
|
-
@streaming_rate = workflow_scope.streaming_rate
|
|
98
|
-
@avg_ttft = workflow_scope.avg_time_to_first_token
|
|
99
|
-
end
|
|
100
|
-
|
|
101
|
-
# Loads available filter options from execution history
|
|
102
|
-
#
|
|
103
|
-
# @return [void]
|
|
104
|
-
def load_filter_options
|
|
105
|
-
filter_data = Execution.by_agent(@workflow_type)
|
|
106
|
-
.where.not(agent_version: nil)
|
|
107
|
-
.or(Execution.by_agent(@workflow_type).where.not(model_id: nil))
|
|
108
|
-
.or(Execution.by_agent(@workflow_type).where.not(temperature: nil))
|
|
109
|
-
.pluck(:agent_version, :model_id, :temperature)
|
|
110
|
-
|
|
111
|
-
@versions = filter_data.map(&:first).compact.uniq.sort.reverse
|
|
112
|
-
@models = filter_data.map { |d| d[1] }.compact.uniq.sort
|
|
113
|
-
@temperatures = filter_data.map(&:last).compact.uniq.sort
|
|
114
|
-
end
|
|
115
|
-
|
|
116
|
-
# Loads paginated and filtered executions with statistics
|
|
117
|
-
#
|
|
118
|
-
# @return [void]
|
|
119
|
-
def load_filtered_executions
|
|
120
|
-
base_scope = build_filtered_scope
|
|
121
|
-
result = paginate(base_scope)
|
|
122
|
-
@executions = result[:records]
|
|
123
|
-
@pagination = result[:pagination]
|
|
124
|
-
|
|
125
|
-
@filter_stats = {
|
|
126
|
-
total_count: result[:pagination][:total_count],
|
|
127
|
-
total_cost: base_scope.sum(:total_cost),
|
|
128
|
-
total_tokens: base_scope.sum(:total_tokens)
|
|
129
|
-
}
|
|
130
|
-
end
|
|
131
|
-
|
|
132
|
-
# Builds a filtered scope for the current workflow's executions
|
|
133
|
-
#
|
|
134
|
-
# @return [ActiveRecord::Relation] Filtered execution scope
|
|
135
|
-
def build_filtered_scope
|
|
136
|
-
scope = Execution.by_agent(@workflow_type)
|
|
137
|
-
|
|
138
|
-
# Apply status filter with validation
|
|
139
|
-
statuses = parse_array_param(:statuses)
|
|
140
|
-
scope = apply_status_filter(scope, statuses) if statuses.any?
|
|
141
|
-
|
|
142
|
-
# Apply version filter
|
|
143
|
-
versions = parse_array_param(:versions)
|
|
144
|
-
scope = scope.where(agent_version: versions) if versions.any?
|
|
145
|
-
|
|
146
|
-
# Apply model filter
|
|
147
|
-
models = parse_array_param(:models)
|
|
148
|
-
scope = scope.where(model_id: models) if models.any?
|
|
149
|
-
|
|
150
|
-
# Apply temperature filter
|
|
151
|
-
temperatures = parse_array_param(:temperatures)
|
|
152
|
-
scope = scope.where(temperature: temperatures) if temperatures.any?
|
|
153
|
-
|
|
154
|
-
# Apply time range filter with validation
|
|
155
|
-
days = parse_days_param
|
|
156
|
-
scope = apply_time_filter(scope, days)
|
|
157
|
-
|
|
158
|
-
scope
|
|
159
|
-
end
|
|
160
|
-
|
|
161
|
-
# Loads chart data for workflow performance visualization
|
|
162
|
-
#
|
|
163
|
-
# @return [void]
|
|
164
|
-
def load_chart_data
|
|
165
|
-
@trend_data = Execution.trend_analysis(agent_type: @workflow_type, days: 30)
|
|
166
|
-
@status_distribution = Execution.by_agent(@workflow_type).group(:status).count
|
|
167
|
-
@finish_reason_distribution = Execution.by_agent(@workflow_type).finish_reason_distribution
|
|
168
|
-
end
|
|
169
|
-
|
|
170
|
-
# Loads per-step/branch statistics for workflow analytics
|
|
171
|
-
#
|
|
172
|
-
# @return [void]
|
|
173
|
-
def load_step_stats
|
|
174
|
-
@step_stats = calculate_step_stats
|
|
175
|
-
end
|
|
176
|
-
|
|
177
|
-
# Calculates per-step/branch performance statistics
|
|
178
|
-
#
|
|
179
|
-
# @return [Array<Hash>] Array of step stats hashes
|
|
180
|
-
def calculate_step_stats
|
|
181
|
-
# Get root workflow executions
|
|
182
|
-
root_executions = Execution.by_agent(@workflow_type)
|
|
183
|
-
.root_executions
|
|
184
|
-
.where("created_at > ?", 30.days.ago)
|
|
185
|
-
.pluck(:id)
|
|
186
|
-
|
|
187
|
-
return [] if root_executions.empty?
|
|
188
|
-
|
|
189
|
-
# Aggregate child execution stats by workflow_step
|
|
190
|
-
child_stats = Execution.where(parent_execution_id: root_executions)
|
|
191
|
-
.group(:workflow_step)
|
|
192
|
-
.select(
|
|
193
|
-
"workflow_step",
|
|
194
|
-
"COUNT(*) as execution_count",
|
|
195
|
-
"AVG(duration_ms) as avg_duration_ms",
|
|
196
|
-
"SUM(total_cost) as total_cost",
|
|
197
|
-
"AVG(total_cost) as avg_cost",
|
|
198
|
-
"SUM(total_tokens) as total_tokens",
|
|
199
|
-
"AVG(total_tokens) as avg_tokens",
|
|
200
|
-
"SUM(CASE WHEN status = 'success' THEN 1 ELSE 0 END) as success_count",
|
|
201
|
-
"SUM(CASE WHEN status = 'error' THEN 1 ELSE 0 END) as error_count"
|
|
202
|
-
)
|
|
203
|
-
|
|
204
|
-
# Get agent type mappings for each step
|
|
205
|
-
step_agent_map = Execution.where(parent_execution_id: root_executions)
|
|
206
|
-
.where.not(workflow_step: nil)
|
|
207
|
-
.group(:workflow_step)
|
|
208
|
-
.pluck(:workflow_step, Arel.sql("MAX(agent_type)"))
|
|
209
|
-
.to_h
|
|
210
|
-
|
|
211
|
-
child_stats.map do |row|
|
|
212
|
-
next if row.workflow_step.blank?
|
|
213
|
-
|
|
214
|
-
execution_count = row.execution_count.to_i
|
|
215
|
-
success_count = row.success_count.to_i
|
|
216
|
-
success_rate = execution_count > 0 ? (success_count.to_f / execution_count * 100).round(1) : 0
|
|
217
|
-
|
|
218
|
-
{
|
|
219
|
-
name: row.workflow_step,
|
|
220
|
-
agent_type: step_agent_map[row.workflow_step],
|
|
221
|
-
execution_count: execution_count,
|
|
222
|
-
success_rate: success_rate,
|
|
223
|
-
avg_duration_ms: row.avg_duration_ms.to_f.round(0),
|
|
224
|
-
total_cost: row.total_cost.to_f.round(4),
|
|
225
|
-
avg_cost: row.avg_cost.to_f.round(6),
|
|
226
|
-
total_tokens: row.total_tokens.to_i,
|
|
227
|
-
avg_tokens: row.avg_tokens.to_f.round(0)
|
|
228
|
-
}
|
|
229
|
-
end.compact
|
|
230
|
-
end
|
|
231
|
-
|
|
232
|
-
# Loads the current workflow class configuration
|
|
233
|
-
#
|
|
234
|
-
# @return [void]
|
|
235
|
-
def load_workflow_config
|
|
236
|
-
@config = {
|
|
237
|
-
# Basic configuration
|
|
238
|
-
version: safe_call(@workflow_class, :version),
|
|
239
|
-
description: safe_call(@workflow_class, :description),
|
|
240
|
-
timeout: safe_call(@workflow_class, :timeout),
|
|
241
|
-
max_cost: safe_call(@workflow_class, :max_cost)
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
# Load unified workflow structure for all types
|
|
245
|
-
load_unified_workflow_config
|
|
246
|
-
end
|
|
247
|
-
|
|
248
|
-
# Loads unified workflow configuration for all workflow types
|
|
249
|
-
#
|
|
250
|
-
# @return [void]
|
|
251
|
-
def load_unified_workflow_config
|
|
252
|
-
@parallel_groups = []
|
|
253
|
-
@input_schema_fields = {}
|
|
254
|
-
@lifecycle_hooks = {}
|
|
255
|
-
|
|
256
|
-
# All workflows use DSL
|
|
257
|
-
@steps = extract_dsl_steps(@workflow_class)
|
|
258
|
-
@parallel_groups = extract_parallel_groups(@workflow_class)
|
|
259
|
-
@lifecycle_hooks = extract_lifecycle_hooks(@workflow_class)
|
|
260
|
-
|
|
261
|
-
@config[:steps_count] = @steps.size
|
|
262
|
-
@config[:parallel_groups_count] = @parallel_groups.size
|
|
263
|
-
@config[:has_routing] = @steps.any? { |s| s[:routing] }
|
|
264
|
-
@config[:has_conditions] = @steps.any? { |s| s[:if_condition] || s[:unless_condition] }
|
|
265
|
-
@config[:has_retries] = @steps.any? { |s| s[:retry_config] }
|
|
266
|
-
@config[:has_fallbacks] = @steps.any? { |s| s[:fallbacks]&.any? }
|
|
267
|
-
@config[:has_lifecycle_hooks] = @lifecycle_hooks.values.any? { |v| v.to_i > 0 }
|
|
268
|
-
@config[:has_input_schema] = @workflow_class.respond_to?(:input_schema) && @workflow_class.input_schema.present?
|
|
269
|
-
|
|
270
|
-
if @config[:has_input_schema]
|
|
271
|
-
@input_schema_fields = @workflow_class.input_schema.fields.transform_values(&:to_h)
|
|
272
|
-
end
|
|
273
|
-
end
|
|
274
|
-
|
|
275
|
-
# Extracts steps from a DSL-based workflow class with full configuration
|
|
276
|
-
#
|
|
277
|
-
# @param klass [Class] The workflow class
|
|
278
|
-
# @return [Array<Hash>] Array of step hashes with full DSL metadata
|
|
279
|
-
def extract_dsl_steps(klass)
|
|
280
|
-
return [] unless klass.respond_to?(:step_metadata) && klass.respond_to?(:step_configs)
|
|
281
|
-
|
|
282
|
-
step_configs = klass.step_configs
|
|
283
|
-
|
|
284
|
-
klass.step_metadata.map do |meta|
|
|
285
|
-
# Handle wait steps - they have type: :wait and aren't in step_configs
|
|
286
|
-
if meta[:type] == :wait
|
|
287
|
-
{
|
|
288
|
-
name: meta[:name],
|
|
289
|
-
type: :wait,
|
|
290
|
-
wait_type: meta[:wait_type],
|
|
291
|
-
ui_label: meta[:ui_label],
|
|
292
|
-
timeout: meta[:timeout],
|
|
293
|
-
duration: meta[:duration],
|
|
294
|
-
poll_interval: meta[:poll_interval],
|
|
295
|
-
on_timeout: meta[:on_timeout],
|
|
296
|
-
notify: meta[:notify],
|
|
297
|
-
approvers: meta[:approvers],
|
|
298
|
-
parallel: false
|
|
299
|
-
}.compact
|
|
300
|
-
else
|
|
301
|
-
config = step_configs[meta[:name]]
|
|
302
|
-
step_hash = {
|
|
303
|
-
name: meta[:name],
|
|
304
|
-
agent: meta[:agent],
|
|
305
|
-
description: meta[:description],
|
|
306
|
-
ui_label: meta[:ui_label],
|
|
307
|
-
optional: meta[:optional],
|
|
308
|
-
timeout: meta[:timeout],
|
|
309
|
-
routing: meta[:routing],
|
|
310
|
-
parallel: meta[:parallel],
|
|
311
|
-
parallel_group: meta[:parallel_group],
|
|
312
|
-
custom_block: config&.custom_block?,
|
|
313
|
-
# New composition features
|
|
314
|
-
workflow: meta[:workflow],
|
|
315
|
-
iteration: meta[:iteration],
|
|
316
|
-
iteration_concurrency: meta[:iteration_concurrency]
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
# Add extended configuration from StepConfig
|
|
320
|
-
if config
|
|
321
|
-
step_hash.merge!(
|
|
322
|
-
retry_config: extract_retry_config(config),
|
|
323
|
-
fallbacks: config.fallbacks.map(&:name),
|
|
324
|
-
if_condition: describe_condition(config.if_condition),
|
|
325
|
-
unless_condition: describe_condition(config.unless_condition),
|
|
326
|
-
has_input_mapper: config.input_mapper.present?,
|
|
327
|
-
pick_fields: config.pick_fields,
|
|
328
|
-
pick_from: config.pick_from,
|
|
329
|
-
default_value: config.default_value,
|
|
330
|
-
routes: extract_routes(config),
|
|
331
|
-
# Iteration error handling
|
|
332
|
-
iteration_fail_fast: config.iteration_fail_fast?,
|
|
333
|
-
continue_on_error: config.continue_on_error?
|
|
334
|
-
)
|
|
335
|
-
|
|
336
|
-
# Add sub-workflow metadata for nested workflow steps
|
|
337
|
-
if config.workflow? && config.agent
|
|
338
|
-
step_hash[:sub_workflow] = extract_sub_workflow_metadata(config.agent)
|
|
339
|
-
end
|
|
340
|
-
end
|
|
341
|
-
|
|
342
|
-
step_hash.compact
|
|
343
|
-
end
|
|
344
|
-
end
|
|
345
|
-
end
|
|
346
|
-
|
|
347
|
-
# Extracts retry configuration in a display-friendly format
|
|
348
|
-
#
|
|
349
|
-
# @param config [StepConfig] The step configuration
|
|
350
|
-
# @return [Hash, nil] Retry config hash or nil
|
|
351
|
-
def extract_retry_config(config)
|
|
352
|
-
retry_cfg = config.retry_config
|
|
353
|
-
return nil unless retry_cfg && retry_cfg[:max].to_i > 0
|
|
354
|
-
|
|
355
|
-
{
|
|
356
|
-
max: retry_cfg[:max],
|
|
357
|
-
backoff: retry_cfg[:backoff],
|
|
358
|
-
delay: retry_cfg[:delay]
|
|
359
|
-
}
|
|
360
|
-
end
|
|
361
|
-
|
|
362
|
-
# Describes a condition for display
|
|
363
|
-
#
|
|
364
|
-
# @param condition [Symbol, Proc, nil] The condition
|
|
365
|
-
# @return [String, nil] Human-readable description
|
|
366
|
-
def describe_condition(condition)
|
|
367
|
-
return nil if condition.nil?
|
|
368
|
-
|
|
369
|
-
case condition
|
|
370
|
-
when Symbol then condition.to_s
|
|
371
|
-
when Proc then "lambda"
|
|
372
|
-
else condition.to_s
|
|
373
|
-
end
|
|
374
|
-
end
|
|
375
|
-
|
|
376
|
-
# Extracts routes from a routing step
|
|
377
|
-
#
|
|
378
|
-
# @param config [StepConfig] The step configuration
|
|
379
|
-
# @return [Array<Hash>, nil] Array of route hashes or nil
|
|
380
|
-
def extract_routes(config)
|
|
381
|
-
return nil unless config.routing? && config.block
|
|
382
|
-
|
|
383
|
-
builder = RubyLLM::Agents::Workflow::DSL::RouteBuilder.new
|
|
384
|
-
config.block.call(builder)
|
|
385
|
-
|
|
386
|
-
routes = builder.routes.map do |name, route_config|
|
|
387
|
-
{
|
|
388
|
-
name: name.to_s,
|
|
389
|
-
agent: route_config[:agent]&.name,
|
|
390
|
-
timeout: extract_timeout_value(route_config[:options][:timeout]),
|
|
391
|
-
fallback: Array(route_config[:options][:fallback]).first&.then { |f| f.respond_to?(:name) ? f.name : f.to_s },
|
|
392
|
-
has_input_mapper: route_config[:options][:input].present?,
|
|
393
|
-
if_condition: describe_condition(route_config[:options][:if]),
|
|
394
|
-
default: false
|
|
395
|
-
}.compact
|
|
396
|
-
end
|
|
397
|
-
|
|
398
|
-
# Add default route
|
|
399
|
-
if builder.default
|
|
400
|
-
routes << {
|
|
401
|
-
name: "default",
|
|
402
|
-
agent: builder.default[:agent]&.name,
|
|
403
|
-
timeout: extract_timeout_value(builder.default[:options][:timeout]),
|
|
404
|
-
has_input_mapper: builder.default[:options][:input].present?,
|
|
405
|
-
default: true
|
|
406
|
-
}.compact
|
|
407
|
-
end
|
|
408
|
-
|
|
409
|
-
routes
|
|
410
|
-
rescue StandardError => e
|
|
411
|
-
Rails.logger.debug "[RubyLLM::Agents] Could not extract routes: #{e.message}"
|
|
412
|
-
nil
|
|
413
|
-
end
|
|
414
|
-
|
|
415
|
-
# Extracts timeout value handling ActiveSupport::Duration
|
|
416
|
-
#
|
|
417
|
-
# @param timeout [Integer, ActiveSupport::Duration, nil] The timeout value
|
|
418
|
-
# @return [Integer, nil] Timeout in seconds or nil
|
|
419
|
-
def extract_timeout_value(timeout)
|
|
420
|
-
return nil if timeout.nil?
|
|
421
|
-
|
|
422
|
-
timeout.respond_to?(:to_i) ? timeout.to_i : timeout
|
|
423
|
-
end
|
|
424
|
-
|
|
425
|
-
# Extracts metadata for a nested sub-workflow
|
|
426
|
-
#
|
|
427
|
-
# @param workflow_class [Class] The sub-workflow class
|
|
428
|
-
# @return [Hash] Sub-workflow metadata including steps preview and budget info
|
|
429
|
-
def extract_sub_workflow_metadata(workflow_class)
|
|
430
|
-
return nil unless workflow_class.respond_to?(:step_metadata)
|
|
431
|
-
|
|
432
|
-
{
|
|
433
|
-
name: workflow_class.name,
|
|
434
|
-
description: safe_call(workflow_class, :description),
|
|
435
|
-
timeout: safe_call(workflow_class, :timeout),
|
|
436
|
-
max_cost: safe_call(workflow_class, :max_cost),
|
|
437
|
-
max_recursion_depth: safe_call(workflow_class, :max_recursion_depth),
|
|
438
|
-
steps_count: workflow_class.step_configs.size,
|
|
439
|
-
steps_preview: extract_sub_workflow_steps_preview(workflow_class)
|
|
440
|
-
}.compact
|
|
441
|
-
rescue StandardError => e
|
|
442
|
-
Rails.logger.debug "[RubyLLM::Agents] Could not extract sub-workflow metadata: #{e.message}"
|
|
443
|
-
nil
|
|
444
|
-
end
|
|
445
|
-
|
|
446
|
-
# Extracts a simplified steps preview for sub-workflow display
|
|
447
|
-
#
|
|
448
|
-
# @param workflow_class [Class] The sub-workflow class
|
|
449
|
-
# @return [Array<Hash>] Simplified step hashes for preview
|
|
450
|
-
def extract_sub_workflow_steps_preview(workflow_class)
|
|
451
|
-
return [] unless workflow_class.respond_to?(:step_metadata)
|
|
452
|
-
|
|
453
|
-
workflow_class.step_metadata.map do |meta|
|
|
454
|
-
{
|
|
455
|
-
name: meta[:name],
|
|
456
|
-
agent: meta[:agent]&.gsub(/Agent$/, "")&.gsub(/Workflow$/, ""),
|
|
457
|
-
routing: meta[:routing],
|
|
458
|
-
iteration: meta[:iteration],
|
|
459
|
-
workflow: meta[:workflow],
|
|
460
|
-
parallel: meta[:parallel]
|
|
461
|
-
}.compact
|
|
462
|
-
end
|
|
463
|
-
rescue StandardError
|
|
464
|
-
[]
|
|
465
|
-
end
|
|
466
|
-
|
|
467
|
-
# Extracts parallel groups from a DSL-based workflow class
|
|
468
|
-
#
|
|
469
|
-
# @param klass [Class] The workflow class
|
|
470
|
-
# @return [Array<Hash>] Array of parallel group hashes
|
|
471
|
-
def extract_parallel_groups(klass)
|
|
472
|
-
return [] unless klass.respond_to?(:parallel_groups)
|
|
473
|
-
|
|
474
|
-
klass.parallel_groups.map(&:to_h)
|
|
475
|
-
end
|
|
476
|
-
|
|
477
|
-
# Extracts lifecycle hooks from a workflow class
|
|
478
|
-
#
|
|
479
|
-
# @param klass [Class] The workflow class
|
|
480
|
-
# @return [Hash] Hash of hook types to counts
|
|
481
|
-
def extract_lifecycle_hooks(klass)
|
|
482
|
-
return {} unless klass.respond_to?(:lifecycle_hooks)
|
|
483
|
-
|
|
484
|
-
hooks = klass.lifecycle_hooks
|
|
485
|
-
{
|
|
486
|
-
before_workflow: hooks[:before_workflow]&.size || 0,
|
|
487
|
-
after_workflow: hooks[:after_workflow]&.size || 0,
|
|
488
|
-
on_step_error: hooks[:on_step_error]&.size || 0
|
|
489
|
-
}
|
|
490
|
-
end
|
|
491
|
-
|
|
492
|
-
# Safely calls a method on a class, returning nil if method doesn't exist
|
|
493
|
-
#
|
|
494
|
-
# @param klass [Class, nil] The class to call the method on
|
|
495
|
-
# @param method_name [Symbol] The method to call
|
|
496
|
-
# @return [Object, nil] The result or nil
|
|
497
|
-
def safe_call(klass, method_name)
|
|
498
|
-
return nil unless klass
|
|
499
|
-
return nil unless klass.respond_to?(method_name)
|
|
500
|
-
|
|
501
|
-
klass.public_send(method_name)
|
|
502
|
-
rescue StandardError
|
|
503
|
-
nil
|
|
504
|
-
end
|
|
505
|
-
|
|
506
|
-
# Parses and validates sort parameters from request
|
|
507
|
-
#
|
|
508
|
-
# @return [Hash] Hash with :column and :direction keys
|
|
509
|
-
def parse_sort_params
|
|
510
|
-
allowed_columns = %w[name workflow_type execution_count total_cost success_rate last_executed]
|
|
511
|
-
column = params[:sort]
|
|
512
|
-
direction = params[:direction]
|
|
513
|
-
|
|
514
|
-
{
|
|
515
|
-
column: allowed_columns.include?(column) ? column : "name",
|
|
516
|
-
direction: %w[asc desc].include?(direction) ? direction : "asc"
|
|
517
|
-
}
|
|
518
|
-
end
|
|
519
|
-
|
|
520
|
-
# Sorts workflows array by the specified column and direction
|
|
521
|
-
#
|
|
522
|
-
# @param workflows [Array<Hash>] Array of workflow hashes
|
|
523
|
-
# @return [Array<Hash>] Sorted array
|
|
524
|
-
def sort_workflows(workflows)
|
|
525
|
-
column = @sort_params[:column].to_sym
|
|
526
|
-
direction = @sort_params[:direction]
|
|
527
|
-
|
|
528
|
-
sorted = workflows.sort_by do |w|
|
|
529
|
-
value = w[column]
|
|
530
|
-
case column
|
|
531
|
-
when :name
|
|
532
|
-
value.to_s.downcase
|
|
533
|
-
when :last_executed
|
|
534
|
-
value || Time.at(0)
|
|
535
|
-
else
|
|
536
|
-
value || 0
|
|
537
|
-
end
|
|
538
|
-
end
|
|
539
|
-
|
|
540
|
-
direction == "desc" ? sorted.reverse : sorted
|
|
541
|
-
end
|
|
542
|
-
end
|
|
543
|
-
end
|
|
544
|
-
end
|
|
@@ -1,84 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module RubyLLM
|
|
4
|
-
module Agents
|
|
5
|
-
# Mailer for sending alert notifications via email
|
|
6
|
-
#
|
|
7
|
-
# Delivers alert notifications when important events occur like
|
|
8
|
-
# budget exceedance or circuit breaker activation.
|
|
9
|
-
#
|
|
10
|
-
# @example Sending an alert email
|
|
11
|
-
# AlertMailer.alert_notification(
|
|
12
|
-
# event: :budget_hard_cap,
|
|
13
|
-
# payload: { limit: 100.0, total: 105.0 },
|
|
14
|
-
# recipient: "admin@example.com"
|
|
15
|
-
# ).deliver_later
|
|
16
|
-
#
|
|
17
|
-
# @api public
|
|
18
|
-
class AlertMailer < ApplicationMailer
|
|
19
|
-
# Sends an alert notification email
|
|
20
|
-
#
|
|
21
|
-
# @param event [Symbol] The event type (e.g., :budget_soft_cap, :breaker_open)
|
|
22
|
-
# @param payload [Hash] Event-specific data
|
|
23
|
-
# @param recipient [String] Email address of the recipient
|
|
24
|
-
# @return [Mail::Message]
|
|
25
|
-
def alert_notification(event:, payload:, recipient:)
|
|
26
|
-
@event = event
|
|
27
|
-
@payload = payload
|
|
28
|
-
@title = event_title(event)
|
|
29
|
-
@severity = event_severity(event)
|
|
30
|
-
@color = event_color(event)
|
|
31
|
-
@timestamp = Time.current
|
|
32
|
-
|
|
33
|
-
mail(
|
|
34
|
-
to: recipient,
|
|
35
|
-
subject: "[RubyLLM::Agents Alert] #{@title}"
|
|
36
|
-
)
|
|
37
|
-
end
|
|
38
|
-
|
|
39
|
-
private
|
|
40
|
-
|
|
41
|
-
# Returns human-readable title for event type
|
|
42
|
-
#
|
|
43
|
-
# @param event [Symbol] The event type
|
|
44
|
-
# @return [String] Human-readable title
|
|
45
|
-
def event_title(event)
|
|
46
|
-
case event
|
|
47
|
-
when :budget_soft_cap then "Budget Soft Cap Reached"
|
|
48
|
-
when :budget_hard_cap then "Budget Hard Cap Exceeded"
|
|
49
|
-
when :breaker_open then "Circuit Breaker Opened"
|
|
50
|
-
when :agent_anomaly then "Agent Anomaly Detected"
|
|
51
|
-
else event.to_s.titleize
|
|
52
|
-
end
|
|
53
|
-
end
|
|
54
|
-
|
|
55
|
-
# Returns severity level for event type
|
|
56
|
-
#
|
|
57
|
-
# @param event [Symbol] The event type
|
|
58
|
-
# @return [String] Severity level
|
|
59
|
-
def event_severity(event)
|
|
60
|
-
case event
|
|
61
|
-
when :budget_soft_cap then "Warning"
|
|
62
|
-
when :budget_hard_cap then "Critical"
|
|
63
|
-
when :breaker_open then "Critical"
|
|
64
|
-
when :agent_anomaly then "Warning"
|
|
65
|
-
else "Info"
|
|
66
|
-
end
|
|
67
|
-
end
|
|
68
|
-
|
|
69
|
-
# Returns color for event type
|
|
70
|
-
#
|
|
71
|
-
# @param event [Symbol] The event type
|
|
72
|
-
# @return [String] Hex color code
|
|
73
|
-
def event_color(event)
|
|
74
|
-
case event
|
|
75
|
-
when :budget_soft_cap then "#FFA500" # Orange
|
|
76
|
-
when :budget_hard_cap then "#FF0000" # Red
|
|
77
|
-
when :breaker_open then "#FF0000" # Red
|
|
78
|
-
when :agent_anomaly then "#FFA500" # Orange
|
|
79
|
-
else "#0000FF" # Blue
|
|
80
|
-
end
|
|
81
|
-
end
|
|
82
|
-
end
|
|
83
|
-
end
|
|
84
|
-
end
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module RubyLLM
|
|
4
|
-
module Agents
|
|
5
|
-
# Base mailer class for RubyLLM::Agents
|
|
6
|
-
#
|
|
7
|
-
# Host application must configure ActionMailer with SMTP settings
|
|
8
|
-
# for email delivery to work.
|
|
9
|
-
#
|
|
10
|
-
# @api private
|
|
11
|
-
class ApplicationMailer < ::ActionMailer::Base
|
|
12
|
-
default from: -> { default_from_address }
|
|
13
|
-
|
|
14
|
-
layout false # Templates are self-contained
|
|
15
|
-
|
|
16
|
-
private
|
|
17
|
-
|
|
18
|
-
def default_from_address
|
|
19
|
-
RubyLLM::Agents.configuration.alerts&.dig(:email_from) ||
|
|
20
|
-
"noreply@#{default_host}"
|
|
21
|
-
end
|
|
22
|
-
|
|
23
|
-
def default_host
|
|
24
|
-
::ActionMailer::Base.default_url_options[:host] || "example.com"
|
|
25
|
-
end
|
|
26
|
-
end
|
|
27
|
-
end
|
|
28
|
-
end
|