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