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.
Files changed (192) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +101 -334
  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 +46 -10
  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 +87 -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 +528 -989
  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 +9 -14
  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 +9 -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 +58 -262
  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 +52 -6
  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 +58 -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/attempt_tracker.rb +1 -0
  99. data/lib/ruby_llm/agents/infrastructure/budget/budget_query.rb +66 -2
  100. data/lib/ruby_llm/agents/infrastructure/budget/spend_recorder.rb +0 -12
  101. data/lib/ruby_llm/agents/infrastructure/circuit_breaker.rb +10 -13
  102. data/lib/ruby_llm/agents/infrastructure/reliability.rb +37 -2
  103. data/lib/ruby_llm/agents/pipeline/context.rb +0 -1
  104. data/lib/ruby_llm/agents/pipeline/middleware/budget.rb +28 -4
  105. data/lib/ruby_llm/agents/pipeline/middleware/cache.rb +3 -10
  106. data/lib/ruby_llm/agents/pipeline/middleware/instrumentation.rb +88 -55
  107. data/lib/ruby_llm/agents/pipeline/middleware/tenant.rb +5 -41
  108. data/lib/ruby_llm/agents/rails/engine.rb +6 -6
  109. data/lib/ruby_llm/agents/results/base.rb +1 -49
  110. data/lib/ruby_llm/agents/text/embedder.rb +0 -1
  111. data/lib/ruby_llm/agents.rb +1 -9
  112. data/lib/tasks/ruby_llm_agents.rake +34 -0
  113. metadata +12 -81
  114. data/app/controllers/ruby_llm/agents/api_configurations_controller.rb +0 -214
  115. data/app/controllers/ruby_llm/agents/workflows_controller.rb +0 -544
  116. data/app/mailers/ruby_llm/agents/alert_mailer.rb +0 -84
  117. data/app/mailers/ruby_llm/agents/application_mailer.rb +0 -28
  118. data/app/models/ruby_llm/agents/api_configuration.rb +0 -386
  119. data/app/models/ruby_llm/agents/execution/workflow.rb +0 -170
  120. data/app/models/ruby_llm/agents/tenant/configurable.rb +0 -135
  121. data/app/views/ruby_llm/agents/agents/_agent.html.erb +0 -98
  122. data/app/views/ruby_llm/agents/agents/_version_comparison.html.erb +0 -186
  123. data/app/views/ruby_llm/agents/agents/_workflow.html.erb +0 -126
  124. data/app/views/ruby_llm/agents/alert_mailer/alert_notification.html.erb +0 -107
  125. data/app/views/ruby_llm/agents/alert_mailer/alert_notification.text.erb +0 -18
  126. data/app/views/ruby_llm/agents/api_configurations/_api_key_field.html.erb +0 -34
  127. data/app/views/ruby_llm/agents/api_configurations/_form.html.erb +0 -288
  128. data/app/views/ruby_llm/agents/api_configurations/edit.html.erb +0 -95
  129. data/app/views/ruby_llm/agents/api_configurations/edit_tenant.html.erb +0 -97
  130. data/app/views/ruby_llm/agents/api_configurations/show.html.erb +0 -214
  131. data/app/views/ruby_llm/agents/api_configurations/tenant.html.erb +0 -179
  132. data/app/views/ruby_llm/agents/dashboard/_agent_comparison.html.erb +0 -73
  133. data/app/views/ruby_llm/agents/dashboard/_alerts_feed.html.erb +0 -62
  134. data/app/views/ruby_llm/agents/dashboard/_breaker_strip.html.erb +0 -47
  135. data/app/views/ruby_llm/agents/dashboard/_budgets_bar.html.erb +0 -75
  136. data/app/views/ruby_llm/agents/dashboard/_model_comparison.html.erb +0 -56
  137. data/app/views/ruby_llm/agents/dashboard/_model_cost_breakdown.html.erb +0 -115
  138. data/app/views/ruby_llm/agents/dashboard/_now_strip.html.erb +0 -59
  139. data/app/views/ruby_llm/agents/dashboard/_top_errors.html.erb +0 -60
  140. data/app/views/ruby_llm/agents/executions/_workflow_summary.html.erb +0 -86
  141. data/app/views/ruby_llm/agents/executions/dry_run.html.erb +0 -149
  142. data/app/views/ruby_llm/agents/shared/_breadcrumbs.html.erb +0 -48
  143. data/app/views/ruby_llm/agents/shared/_nav_link.html.erb +0 -27
  144. data/app/views/ruby_llm/agents/shared/_stat_card.html.erb +0 -14
  145. data/app/views/ruby_llm/agents/shared/_workflow_type_badge.html.erb +0 -35
  146. data/app/views/ruby_llm/agents/workflows/_empty_state.html.erb +0 -22
  147. data/app/views/ruby_llm/agents/workflows/_step_performance.html.erb +0 -228
  148. data/app/views/ruby_llm/agents/workflows/_structure_dsl.html.erb +0 -539
  149. data/app/views/ruby_llm/agents/workflows/_structure_parallel.html.erb +0 -76
  150. data/app/views/ruby_llm/agents/workflows/_structure_pipeline.html.erb +0 -74
  151. data/app/views/ruby_llm/agents/workflows/_structure_router.html.erb +0 -108
  152. data/app/views/ruby_llm/agents/workflows/_workflow_diagram.html.erb +0 -920
  153. data/app/views/ruby_llm/agents/workflows/index.html.erb +0 -179
  154. data/app/views/ruby_llm/agents/workflows/show.html.erb +0 -467
  155. data/lib/generators/ruby_llm_agents/api_configuration_generator.rb +0 -100
  156. data/lib/generators/ruby_llm_agents/templates/add_workflow_migration.rb.tt +0 -38
  157. data/lib/generators/ruby_llm_agents/templates/application_workflow.rb.tt +0 -48
  158. data/lib/generators/ruby_llm_agents/templates/create_api_configurations_migration.rb.tt +0 -90
  159. data/lib/generators/ruby_llm_agents/templates/skills/WORKFLOWS.md.tt +0 -551
  160. data/lib/ruby_llm/agents/core/base/moderation_dsl.rb +0 -181
  161. data/lib/ruby_llm/agents/core/base/moderation_execution.rb +0 -274
  162. data/lib/ruby_llm/agents/core/resolved_config.rb +0 -348
  163. data/lib/ruby_llm/agents/image/generator/content_policy.rb +0 -95
  164. data/lib/ruby_llm/agents/infrastructure/redactor.rb +0 -130
  165. data/lib/ruby_llm/agents/results/moderation_result.rb +0 -158
  166. data/lib/ruby_llm/agents/text/moderator.rb +0 -237
  167. data/lib/ruby_llm/agents/workflow/approval.rb +0 -205
  168. data/lib/ruby_llm/agents/workflow/approval_store.rb +0 -179
  169. data/lib/ruby_llm/agents/workflow/async.rb +0 -220
  170. data/lib/ruby_llm/agents/workflow/async_executor.rb +0 -156
  171. data/lib/ruby_llm/agents/workflow/dsl/executor.rb +0 -467
  172. data/lib/ruby_llm/agents/workflow/dsl/input_schema.rb +0 -244
  173. data/lib/ruby_llm/agents/workflow/dsl/iteration_executor.rb +0 -289
  174. data/lib/ruby_llm/agents/workflow/dsl/parallel_group.rb +0 -107
  175. data/lib/ruby_llm/agents/workflow/dsl/route_builder.rb +0 -150
  176. data/lib/ruby_llm/agents/workflow/dsl/schedule_helpers.rb +0 -187
  177. data/lib/ruby_llm/agents/workflow/dsl/step_config.rb +0 -352
  178. data/lib/ruby_llm/agents/workflow/dsl/step_executor.rb +0 -415
  179. data/lib/ruby_llm/agents/workflow/dsl/wait_config.rb +0 -257
  180. data/lib/ruby_llm/agents/workflow/dsl/wait_executor.rb +0 -317
  181. data/lib/ruby_llm/agents/workflow/dsl.rb +0 -576
  182. data/lib/ruby_llm/agents/workflow/instrumentation.rb +0 -249
  183. data/lib/ruby_llm/agents/workflow/notifiers/base.rb +0 -117
  184. data/lib/ruby_llm/agents/workflow/notifiers/email.rb +0 -117
  185. data/lib/ruby_llm/agents/workflow/notifiers/slack.rb +0 -180
  186. data/lib/ruby_llm/agents/workflow/notifiers/webhook.rb +0 -121
  187. data/lib/ruby_llm/agents/workflow/notifiers.rb +0 -70
  188. data/lib/ruby_llm/agents/workflow/orchestrator.rb +0 -416
  189. data/lib/ruby_llm/agents/workflow/result.rb +0 -592
  190. data/lib/ruby_llm/agents/workflow/thread_pool.rb +0 -185
  191. data/lib/ruby_llm/agents/workflow/throttle_manager.rb +0 -206
  192. data/lib/ruby_llm/agents/workflow/wait_result.rb +0 -213
@@ -1,249 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module RubyLLM
4
- module Agents
5
- class Workflow
6
- # Instrumentation concern for workflow execution tracking
7
- #
8
- # Provides comprehensive workflow tracking including:
9
- # - Root execution record creation for the workflow
10
- # - Timing metrics (started_at, completed_at, duration_ms)
11
- # - Aggregate token usage and cost across all steps/branches
12
- # - Workflow-specific metadata (workflow_id, workflow_type)
13
- # - Error handling with proper status updates
14
- #
15
- # @api private
16
- module Instrumentation
17
- extend ActiveSupport::Concern
18
-
19
- included do
20
- # @!attribute [rw] execution_id
21
- # The ID of the workflow's root execution record
22
- # @return [Integer, nil]
23
- attr_accessor :execution_id
24
- end
25
-
26
- # Wraps workflow execution with comprehensive metrics tracking
27
- #
28
- # Creates a root execution record for the workflow and tracks
29
- # aggregate metrics from all child executions.
30
- #
31
- # @yield The block containing the workflow execution
32
- # @return [WorkflowResult] The workflow result
33
- def instrument_workflow(&block)
34
- started_at = Time.current
35
- @workflow_started_at = started_at
36
-
37
- # Create workflow execution record
38
- execution = create_workflow_execution(started_at)
39
- @execution_id = execution&.id
40
- @root_execution_id = execution&.id
41
-
42
- begin
43
- result = if self.class.timeout
44
- Timeout.timeout(self.class.timeout) { yield }
45
- else
46
- yield
47
- end
48
-
49
- complete_workflow_execution(
50
- execution,
51
- completed_at: Time.current,
52
- status: result.status,
53
- result: result
54
- )
55
-
56
- result
57
- rescue Timeout::Error => e
58
- complete_workflow_execution(
59
- execution,
60
- completed_at: Time.current,
61
- status: "timeout",
62
- error: e
63
- )
64
- raise
65
- rescue WorkflowCostExceededError => e
66
- complete_workflow_execution(
67
- execution,
68
- completed_at: Time.current,
69
- status: "error",
70
- error: e
71
- )
72
- raise
73
- rescue StandardError => e
74
- complete_workflow_execution(
75
- execution,
76
- completed_at: Time.current,
77
- status: "error",
78
- error: e
79
- )
80
- raise
81
- end
82
- end
83
-
84
- private
85
-
86
- # Creates the initial workflow execution record
87
- #
88
- # @param started_at [Time] When the workflow started
89
- # @return [RubyLLM::Agents::Execution, nil] The created record
90
- def create_workflow_execution(started_at)
91
- RubyLLM::Agents::Execution.create!(
92
- agent_type: self.class.name,
93
- agent_version: self.class.version,
94
- model_id: "workflow",
95
- temperature: nil,
96
- started_at: started_at,
97
- status: "running",
98
- parameters: Redactor.redact(options),
99
- metadata: workflow_metadata,
100
- workflow_id: workflow_id,
101
- workflow_type: workflow_type_name
102
- )
103
- rescue StandardError => e
104
- Rails.logger.error("[RubyLLM::Agents::Workflow] Failed to create workflow execution: #{e.message}")
105
- nil
106
- end
107
-
108
- # Updates the workflow execution record with completion data
109
- #
110
- # @param execution [Execution, nil] The execution record
111
- # @param completed_at [Time] When the workflow completed
112
- # @param status [String] Final status
113
- # @param result [WorkflowResult, nil] The workflow result
114
- # @param error [Exception, nil] The error if failed
115
- def complete_workflow_execution(execution, completed_at:, status:, result: nil, error: nil)
116
- return unless execution
117
-
118
- started_at = execution.started_at
119
- duration_ms = ((completed_at - started_at) * 1000).round
120
-
121
- update_data = {
122
- completed_at: completed_at,
123
- duration_ms: duration_ms,
124
- status: status
125
- }
126
-
127
- # Add aggregate metrics from result
128
- if result
129
- update_data.merge!(
130
- input_tokens: result.input_tokens,
131
- output_tokens: result.output_tokens,
132
- total_tokens: result.total_tokens,
133
- cached_tokens: result.cached_tokens,
134
- input_cost: result.input_cost,
135
- output_cost: result.output_cost,
136
- total_cost: result.total_cost
137
- )
138
-
139
- # Store step/branch results summary
140
- update_data[:response] = build_response_summary(result)
141
- end
142
-
143
- # Add error data if failed
144
- if error
145
- update_data.merge!(
146
- error_class: error.class.name,
147
- error_message: error.message.to_s.truncate(65535)
148
- )
149
- end
150
-
151
- execution.update!(update_data)
152
- rescue StandardError => e
153
- Rails.logger.error("[RubyLLM::Agents::Workflow] Failed to update workflow execution #{execution&.id}: #{e.message}")
154
- mark_workflow_failed!(execution, error: error || e)
155
- end
156
-
157
- # Emergency fallback to mark workflow as failed
158
- #
159
- # @param execution [Execution, nil] The execution record
160
- # @param error [Exception, nil] The error
161
- def mark_workflow_failed!(execution, error: nil)
162
- return unless execution&.id
163
-
164
- update_data = {
165
- status: "error",
166
- completed_at: Time.current,
167
- error_class: error&.class&.name || "UnknownError",
168
- error_message: error&.message&.to_s&.truncate(65535) || "Unknown error"
169
- }
170
-
171
- execution.class.where(id: execution.id, status: "running").update_all(update_data)
172
- rescue StandardError => e
173
- Rails.logger.error("[RubyLLM::Agents::Workflow] CRITICAL: Failed to mark workflow #{execution&.id} as failed: #{e.message}")
174
- end
175
-
176
- # Builds a summary of step/branch results for storage
177
- #
178
- # @param result [WorkflowResult] The workflow result
179
- # @return [Hash] Summary data
180
- def build_response_summary(result)
181
- summary = {
182
- workflow_type: result.workflow_type,
183
- status: result.status
184
- }
185
-
186
- if result.steps.any?
187
- summary[:steps] = result.steps.transform_values do |r|
188
- {
189
- status: r.respond_to?(:success?) ? (r.success? ? "success" : "error") : "unknown",
190
- total_cost: r.respond_to?(:total_cost) ? r.total_cost : 0,
191
- duration_ms: r.respond_to?(:duration_ms) ? r.duration_ms : nil
192
- }
193
- end
194
- end
195
-
196
- if result.branches.any?
197
- summary[:branches] = result.branches.transform_values do |r|
198
- next { status: "error" } if r.nil?
199
-
200
- {
201
- status: r.respond_to?(:success?) ? (r.success? ? "success" : "error") : "unknown",
202
- total_cost: r.respond_to?(:total_cost) ? r.total_cost : 0,
203
- duration_ms: r.respond_to?(:duration_ms) ? r.duration_ms : nil
204
- }
205
- end
206
- end
207
-
208
- if result.routed_to
209
- summary[:routed_to] = result.routed_to
210
- summary[:classification_cost] = result.classification_cost
211
- end
212
-
213
- summary
214
- end
215
-
216
- # Returns workflow-specific metadata
217
- #
218
- # @return [Hash] Workflow metadata
219
- def workflow_metadata
220
- base_metadata = {
221
- workflow_id: workflow_id,
222
- workflow_type: workflow_type_name
223
- }
224
-
225
- # Allow subclasses to add custom metadata
226
- if respond_to?(:execution_metadata, true)
227
- base_metadata.merge(execution_metadata)
228
- else
229
- base_metadata
230
- end
231
- end
232
-
233
- # Returns the workflow type name for storage
234
- #
235
- # @return [String] The workflow type
236
- def workflow_type_name
237
- "workflow"
238
- end
239
-
240
- # Hook for subclasses to add custom metadata
241
- #
242
- # @return [Hash] Custom metadata
243
- def execution_metadata
244
- {}
245
- end
246
- end
247
- end
248
- end
249
- end
@@ -1,117 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module RubyLLM
4
- module Agents
5
- class Workflow
6
- module Notifiers
7
- # Base class for approval notification adapters
8
- #
9
- # Subclasses should implement the #notify and optionally #remind methods
10
- # to send notifications through their respective channels.
11
- #
12
- # @example Creating a custom notifier
13
- # class SmsNotifier < Base
14
- # def notify(approval, message)
15
- # # Send SMS via Twilio
16
- # end
17
- # end
18
- #
19
- # @api public
20
- class Base
21
- # Send a notification for an approval request
22
- #
23
- # @param approval [Approval] The approval request
24
- # @param message [String] The notification message
25
- # @return [Boolean] true if notification was sent successfully
26
- def notify(approval, message)
27
- raise NotImplementedError, "#{self.class}#notify must be implemented"
28
- end
29
-
30
- # Send a reminder for a pending approval
31
- #
32
- # @param approval [Approval] The approval request
33
- # @param message [String] The reminder message
34
- # @return [Boolean] true if reminder was sent successfully
35
- def remind(approval, message)
36
- notify(approval, "[Reminder] #{message}")
37
- end
38
-
39
- # Send an escalation notice
40
- #
41
- # @param approval [Approval] The approval request
42
- # @param message [String] The escalation message
43
- # @param escalate_to [String] The escalation target
44
- # @return [Boolean] true if escalation was sent successfully
45
- def escalate(approval, message, escalate_to:)
46
- notify(approval, "[Escalation to #{escalate_to}] #{message}")
47
- end
48
- end
49
-
50
- # Registry for notifier instances
51
- #
52
- # @api private
53
- class Registry
54
- class << self
55
- # Returns the registered notifiers
56
- #
57
- # @return [Hash<Symbol, Base>]
58
- def notifiers
59
- @notifiers ||= {}
60
- end
61
-
62
- # Register a notifier
63
- #
64
- # @param name [Symbol] The notifier name
65
- # @param notifier [Base] The notifier instance
66
- # @return [void]
67
- def register(name, notifier)
68
- notifiers[name.to_sym] = notifier
69
- end
70
-
71
- # Get a registered notifier
72
- #
73
- # @param name [Symbol] The notifier name
74
- # @return [Base, nil]
75
- def get(name)
76
- notifiers[name.to_sym]
77
- end
78
-
79
- # Check if a notifier is registered
80
- #
81
- # @param name [Symbol] The notifier name
82
- # @return [Boolean]
83
- def registered?(name)
84
- notifiers.key?(name.to_sym)
85
- end
86
-
87
- # Send notification through specified channels
88
- #
89
- # @param approval [Approval] The approval request
90
- # @param message [String] The notification message
91
- # @param channels [Array<Symbol>] The notification channels
92
- # @return [Hash<Symbol, Boolean>] Results per channel
93
- def notify_all(approval, message, channels:)
94
- results = {}
95
- channels.each do |channel|
96
- notifier = get(channel)
97
- if notifier
98
- results[channel] = notifier.notify(approval, message)
99
- else
100
- results[channel] = false
101
- end
102
- end
103
- results
104
- end
105
-
106
- # Reset the registry (useful for testing)
107
- #
108
- # @return [void]
109
- def reset!
110
- @notifiers = {}
111
- end
112
- end
113
- end
114
- end
115
- end
116
- end
117
- end
@@ -1,117 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module RubyLLM
4
- module Agents
5
- class Workflow
6
- module Notifiers
7
- # Email notification adapter for approval requests
8
- #
9
- # Uses ActionMailer if available, or a configured mailer class.
10
- # Can be configured with custom templates and delivery options.
11
- #
12
- # @example Configuration
13
- # RubyLLM::Agents::Workflow::Notifiers::Email.configure do |config|
14
- # config.mailer_class = ApprovalMailer
15
- # config.from = "approvals@example.com"
16
- # end
17
- #
18
- # @api public
19
- class Email < Base
20
- class << self
21
- attr_accessor :mailer_class, :from_address, :subject_prefix
22
-
23
- # Configure the email notifier
24
- #
25
- # @yield [self] The email notifier class
26
- # @return [void]
27
- def configure
28
- yield self
29
- end
30
-
31
- # Reset configuration to defaults
32
- #
33
- # @return [void]
34
- def reset!
35
- @mailer_class = nil
36
- @from_address = nil
37
- @subject_prefix = nil
38
- end
39
- end
40
-
41
- # @param mailer_class [Class, nil] Custom mailer class
42
- # @param from [String, nil] From address
43
- # @param subject_prefix [String, nil] Subject line prefix
44
- def initialize(mailer_class: nil, from: nil, subject_prefix: nil)
45
- @mailer_class = mailer_class || self.class.mailer_class
46
- @from_address = from || self.class.from_address || "noreply@example.com"
47
- @subject_prefix = subject_prefix || self.class.subject_prefix || "[Approval Required]"
48
- end
49
-
50
- # Send an email notification
51
- #
52
- # @param approval [Approval] The approval request
53
- # @param message [String] The notification message
54
- # @return [Boolean] true if email was queued
55
- def notify(approval, message)
56
- if @mailer_class
57
- send_via_mailer(approval, message)
58
- elsif defined?(ActionMailer)
59
- send_via_action_mailer(approval, message)
60
- else
61
- log_notification(approval, message)
62
- false
63
- end
64
- rescue StandardError => e
65
- handle_error(e, approval)
66
- false
67
- end
68
-
69
- private
70
-
71
- def send_via_mailer(approval, message)
72
- if @mailer_class.respond_to?(:approval_request)
73
- mail = @mailer_class.approval_request(approval, message)
74
- deliver_mail(mail)
75
- true
76
- else
77
- false
78
- end
79
- end
80
-
81
- def send_via_action_mailer(approval, message)
82
- # Generic ActionMailer support if no custom mailer is configured
83
- # Applications should configure a mailer_class for production use
84
- log_notification(approval, message)
85
- false
86
- end
87
-
88
- def deliver_mail(mail)
89
- if mail.respond_to?(:deliver_later)
90
- mail.deliver_later
91
- elsif mail.respond_to?(:deliver_now)
92
- mail.deliver_now
93
- elsif mail.respond_to?(:deliver)
94
- mail.deliver
95
- end
96
- end
97
-
98
- def log_notification(approval, message)
99
- if defined?(Rails) && Rails.logger
100
- Rails.logger.info(
101
- "[RubyLLM::Agents] Email notification for approval #{approval.id}: #{message}"
102
- )
103
- end
104
- end
105
-
106
- def handle_error(error, approval)
107
- if defined?(Rails) && Rails.logger
108
- Rails.logger.error(
109
- "[RubyLLM::Agents] Failed to send email for approval #{approval.id}: #{error.message}"
110
- )
111
- end
112
- end
113
- end
114
- end
115
- end
116
- end
117
- end
@@ -1,180 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "net/http"
4
- require "uri"
5
- require "json"
6
-
7
- module RubyLLM
8
- module Agents
9
- class Workflow
10
- module Notifiers
11
- # Slack notification adapter for approval requests
12
- #
13
- # Sends notifications via Slack webhooks or the Slack API.
14
- # Supports rich message formatting with blocks.
15
- #
16
- # @example Using a webhook
17
- # notifier = Slack.new(webhook_url: "https://hooks.slack.com/...")
18
- # notifier.notify(approval, "Please review this request")
19
- #
20
- # @example Using the API
21
- # notifier = Slack.new(api_token: "xoxb-...", channel: "#approvals")
22
- #
23
- # @api public
24
- class Slack < Base
25
- class << self
26
- attr_accessor :webhook_url, :api_token, :default_channel
27
-
28
- # Configure the Slack notifier
29
- #
30
- # @yield [self] The Slack notifier class
31
- # @return [void]
32
- def configure
33
- yield self
34
- end
35
-
36
- # Reset configuration to defaults
37
- #
38
- # @return [void]
39
- def reset!
40
- @webhook_url = nil
41
- @api_token = nil
42
- @default_channel = nil
43
- end
44
- end
45
-
46
- # @param webhook_url [String, nil] Slack webhook URL
47
- # @param api_token [String, nil] Slack API token (for posting via API)
48
- # @param channel [String, nil] Default channel for messages
49
- def initialize(webhook_url: nil, api_token: nil, channel: nil)
50
- @webhook_url = webhook_url || self.class.webhook_url
51
- @api_token = api_token || self.class.api_token
52
- @channel = channel || self.class.default_channel
53
- end
54
-
55
- # Send a Slack notification
56
- #
57
- # @param approval [Approval] The approval request
58
- # @param message [String] The notification message
59
- # @return [Boolean] true if notification was sent
60
- def notify(approval, message)
61
- payload = build_payload(approval, message)
62
-
63
- if @webhook_url
64
- send_webhook(payload)
65
- elsif @api_token
66
- send_api(payload)
67
- else
68
- log_notification(approval, message)
69
- false
70
- end
71
- rescue StandardError => e
72
- handle_error(e, approval)
73
- false
74
- end
75
-
76
- private
77
-
78
- def build_payload(approval, message)
79
- {
80
- text: message,
81
- blocks: build_blocks(approval, message)
82
- }.tap do |payload|
83
- payload[:channel] = @channel if @channel && @api_token
84
- end
85
- end
86
-
87
- def build_blocks(approval, message)
88
- [
89
- {
90
- type: "header",
91
- text: {
92
- type: "plain_text",
93
- text: "Approval Required: #{approval.name}",
94
- emoji: true
95
- }
96
- },
97
- {
98
- type: "section",
99
- text: {
100
- type: "mrkdwn",
101
- text: message
102
- }
103
- },
104
- {
105
- type: "section",
106
- fields: [
107
- {
108
- type: "mrkdwn",
109
- text: "*Workflow:*\n#{approval.workflow_type}"
110
- },
111
- {
112
- type: "mrkdwn",
113
- text: "*Workflow ID:*\n#{approval.workflow_id}"
114
- }
115
- ]
116
- },
117
- {
118
- type: "context",
119
- elements: [
120
- {
121
- type: "mrkdwn",
122
- text: "Approval ID: `#{approval.id}`"
123
- }
124
- ]
125
- }
126
- ]
127
- end
128
-
129
- def send_webhook(payload)
130
- uri = URI.parse(@webhook_url)
131
- http = Net::HTTP.new(uri.host, uri.port)
132
- http.use_ssl = uri.scheme == "https"
133
- http.open_timeout = 5
134
- http.read_timeout = 10
135
-
136
- request = Net::HTTP::Post.new(uri.path)
137
- request["Content-Type"] = "application/json"
138
- request.body = payload.to_json
139
-
140
- response = http.request(request)
141
- response.code.to_i == 200
142
- end
143
-
144
- def send_api(payload)
145
- uri = URI.parse("https://slack.com/api/chat.postMessage")
146
- http = Net::HTTP.new(uri.host, uri.port)
147
- http.use_ssl = true
148
- http.open_timeout = 5
149
- http.read_timeout = 10
150
-
151
- request = Net::HTTP::Post.new(uri.path)
152
- request["Content-Type"] = "application/json"
153
- request["Authorization"] = "Bearer #{@api_token}"
154
- request.body = payload.to_json
155
-
156
- response = http.request(request)
157
- result = JSON.parse(response.body)
158
- result["ok"] == true
159
- end
160
-
161
- def log_notification(approval, message)
162
- if defined?(Rails) && Rails.logger
163
- Rails.logger.info(
164
- "[RubyLLM::Agents] Slack notification for approval #{approval.id}: #{message}"
165
- )
166
- end
167
- end
168
-
169
- def handle_error(error, approval)
170
- if defined?(Rails) && Rails.logger
171
- Rails.logger.error(
172
- "[RubyLLM::Agents] Failed to send Slack message for approval #{approval.id}: #{error.message}"
173
- )
174
- end
175
- end
176
- end
177
- end
178
- end
179
- end
180
- end