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
@@ -15,7 +15,6 @@ module RubyLLM
15
15
  # @example Creating an agent
16
16
  # class SearchAgent < RubyLLM::Agents::BaseAgent
17
17
  # model "gpt-4o"
18
- # version "1.0"
19
18
  # description "Searches for relevant documents"
20
19
  # timeout 30
21
20
  #
@@ -246,24 +245,38 @@ module RubyLLM
246
245
 
247
246
  # User prompt to send to the LLM
248
247
  #
249
- # @abstract Subclasses must implement this method
248
+ # If a class-level `prompt` DSL is defined (string template or block),
249
+ # it will be used. Otherwise, subclasses must implement this method.
250
+ #
250
251
  # @return [String] The user prompt
251
252
  def user_prompt
252
- raise NotImplementedError, "#{self.class} must implement #user_prompt"
253
+ prompt_config = self.class.prompt_config
254
+ return resolve_prompt_from_config(prompt_config) if prompt_config
255
+
256
+ raise NotImplementedError, "#{self.class} must implement #user_prompt or use the prompt DSL"
253
257
  end
254
258
 
255
259
  # System prompt for LLM instructions
256
260
  #
261
+ # If a class-level `system` DSL is defined, it will be used.
262
+ # Otherwise returns nil.
263
+ #
257
264
  # @return [String, nil] System instructions, or nil for none
258
265
  def system_prompt
266
+ system_config = self.class.system_config
267
+ return resolve_prompt_from_config(system_config) if system_config
268
+
259
269
  nil
260
270
  end
261
271
 
262
272
  # Response schema for structured output
263
273
  #
274
+ # Delegates to the class-level schema DSL by default.
275
+ # Override in subclass instances to customize per-instance.
276
+ #
264
277
  # @return [RubyLLM::Schema, nil] Schema definition, or nil for free-form
265
278
  def schema
266
- nil
279
+ self.class.schema
267
280
  end
268
281
 
269
282
  # Conversation history for multi-turn conversations
@@ -288,9 +301,12 @@ module RubyLLM
288
301
 
289
302
  # Generates the cache key for this agent invocation
290
303
  #
291
- # @return [String] Cache key in format "ruby_llm_agent/ClassName/version/hash"
304
+ # Cache keys are content-based, using a hash of the prompts and parameters.
305
+ # This automatically invalidates caches when prompts change.
306
+ #
307
+ # @return [String] Cache key in format "ruby_llm_agent/ClassName/hash"
292
308
  def agent_cache_key
293
- ["ruby_llm_agent", self.class.name, self.class.version, cache_key_hash].join("/")
309
+ ["ruby_llm_agent", self.class.name, cache_key_hash].join("/")
294
310
  end
295
311
 
296
312
  # Generates a hash of the cache key data
@@ -462,6 +478,36 @@ module RubyLLM
462
478
  end
463
479
  end
464
480
 
481
+ # Resolves a prompt from DSL configuration (template string or block)
482
+ #
483
+ # For string templates, interpolates {placeholder} with parameter values.
484
+ # For blocks, evaluates in the instance context.
485
+ #
486
+ # @param config [String, Proc] The prompt configuration
487
+ # @return [String] The resolved prompt
488
+ def resolve_prompt_from_config(config)
489
+ case config
490
+ when String
491
+ interpolate_template(config)
492
+ when Proc
493
+ instance_eval(&config)
494
+ else
495
+ config.to_s
496
+ end
497
+ end
498
+
499
+ # Interpolates {placeholder} patterns in a template string
500
+ #
501
+ # @param template [String] Template with {placeholder} syntax
502
+ # @return [String] Interpolated string
503
+ def interpolate_template(template)
504
+ template.gsub(/\{(\w+)\}/) do
505
+ param_name = ::Regexp.last_match(1).to_sym
506
+ value = send(param_name) if respond_to?(param_name)
507
+ value.to_s
508
+ end
509
+ end
510
+
465
511
  # Execute the core LLM call
466
512
  #
467
513
  # This is called by the Pipeline::Executor after all middleware
@@ -0,0 +1,142 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module Agents
5
+ # DSL and execution support for before_call/after_call hooks
6
+ #
7
+ # Provides callbacks that run before and after the LLM call,
8
+ # allowing custom preprocessing (redaction, moderation, validation)
9
+ # and postprocessing (logging, transformation) logic.
10
+ #
11
+ # @example Using callbacks
12
+ # class MyAgent < ApplicationAgent
13
+ # before_call :sanitize_input
14
+ # after_call :log_response
15
+ #
16
+ # # Or with blocks
17
+ # before_call { |context| context.params[:timestamp] = Time.current }
18
+ # after_call { |context, response| notify(response) }
19
+ #
20
+ # private
21
+ #
22
+ # def sanitize_input(context)
23
+ # # Mutate context as needed
24
+ # # Raise to block execution
25
+ # end
26
+ #
27
+ # def log_response(context, response)
28
+ # Rails.logger.info("Response: #{response}")
29
+ # end
30
+ # end
31
+ #
32
+ module CallbacksDSL
33
+ # Add a callback to run before the LLM call
34
+ #
35
+ # Callbacks receive the pipeline context and can:
36
+ # - Mutate the context (params, prompts, etc.)
37
+ # - Raise an exception to block execution
38
+ # - Return value is ignored
39
+ #
40
+ # @param method_name [Symbol, nil] Instance method to call
41
+ # @yield [context] Block to execute
42
+ # @yieldparam context [Pipeline::Context] The execution context
43
+ # @return [void]
44
+ #
45
+ # @example With method name
46
+ # before_call :validate_input
47
+ #
48
+ # @example With block
49
+ # before_call { |context| context.params[:sanitized] = true }
50
+ #
51
+ def before_call(method_name = nil, &block)
52
+ @callbacks ||= { before: [], after: [] }
53
+ @callbacks[:before] << (block || method_name)
54
+ end
55
+
56
+ # Add a callback to run after the LLM call
57
+ #
58
+ # Callbacks receive the pipeline context and the response.
59
+ # Return value is ignored.
60
+ #
61
+ # @param method_name [Symbol, nil] Instance method to call
62
+ # @yield [context, response] Block to execute
63
+ # @yieldparam context [Pipeline::Context] The execution context
64
+ # @yieldparam response [Object] The LLM response
65
+ # @return [void]
66
+ #
67
+ # @example With method name
68
+ # after_call :log_response
69
+ #
70
+ # @example With block
71
+ # after_call { |context, response| notify_completion(response) }
72
+ #
73
+ def after_call(method_name = nil, &block)
74
+ @callbacks ||= { before: [], after: [] }
75
+ @callbacks[:after] << (block || method_name)
76
+ end
77
+
78
+ # Simplified alias for before_call (block-only)
79
+ #
80
+ # This is the preferred method in the simplified DSL.
81
+ #
82
+ # @yield [context] Block to execute before the LLM call
83
+ # @yieldparam context [Pipeline::Context] The execution context
84
+ # @return [void]
85
+ #
86
+ # @example
87
+ # before { |ctx| ctx.params[:timestamp] = Time.current }
88
+ # before { |ctx| validate_input!(ctx.params[:query]) }
89
+ #
90
+ def before(&block)
91
+ before_call(&block)
92
+ end
93
+
94
+ # Simplified alias for after_call (block-only)
95
+ #
96
+ # This is the preferred method in the simplified DSL.
97
+ #
98
+ # @yield [context, response] Block to execute after the LLM call
99
+ # @yieldparam context [Pipeline::Context] The execution context
100
+ # @yieldparam response [Object] The LLM response
101
+ # @return [void]
102
+ #
103
+ # @example
104
+ # after { |ctx, result| Rails.logger.info("Completed: #{result}") }
105
+ # after { |ctx, result| notify_slack(result) if result.confidence < 0.5 }
106
+ #
107
+ def after(&block)
108
+ after_call(&block)
109
+ end
110
+
111
+ # Get all registered callbacks
112
+ #
113
+ # @return [Hash] Hash with :before and :after arrays
114
+ def callbacks
115
+ @callbacks ||= { before: [], after: [] }
116
+ end
117
+ end
118
+
119
+ # Instance methods for running callbacks
120
+ module CallbacksExecution
121
+ private
122
+
123
+ # Run callbacks of the specified type
124
+ #
125
+ # @param type [Symbol] :before or :after
126
+ # @param args [Array] Arguments to pass to callbacks
127
+ # @return [void]
128
+ def run_callbacks(type, *args)
129
+ callbacks = self.class.callbacks[type] || []
130
+
131
+ callbacks.each do |callback|
132
+ case callback
133
+ when Symbol
134
+ send(callback, *args)
135
+ when Proc
136
+ instance_exec(*args, &callback)
137
+ end
138
+ end
139
+ end
140
+ end
141
+ end
142
+ end
@@ -1,20 +1,18 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "base/moderation_dsl"
4
- require_relative "base/moderation_execution"
3
+ require_relative "base/callbacks"
5
4
 
6
5
  module RubyLLM
7
6
  module Agents
8
7
  # Base class for LLM-powered conversational agents
9
8
  #
10
9
  # Inherits from BaseAgent to use the middleware pipeline architecture
11
- # while adding moderation capabilities for input/output content filtering.
10
+ # while adding callback hooks for custom preprocessing and postprocessing.
12
11
  #
13
12
  # @example Creating an agent
14
13
  # class SearchAgent < ApplicationAgent
15
14
  # model "gpt-4o"
16
15
  # temperature 0.0
17
- # version "1.0"
18
16
  # timeout 30
19
17
  # cache_for 1.hour
20
18
  #
@@ -30,14 +28,19 @@ module RubyLLM
30
28
  # end
31
29
  # end
32
30
  #
33
- # @example With moderation
31
+ # @example With callbacks
34
32
  # class SafeAgent < ApplicationAgent
35
- # moderation :input, :output
36
- # # or
37
- # moderation :both
33
+ # before_call :redact_pii
34
+ # after_call :log_response
38
35
  #
39
- # def user_prompt
40
- # query
36
+ # private
37
+ #
38
+ # def redact_pii(context)
39
+ # # Custom redaction logic
40
+ # end
41
+ #
42
+ # def log_response(context, response)
43
+ # Rails.logger.info("Response received")
41
44
  # end
42
45
  # end
43
46
  #
@@ -49,8 +52,8 @@ module RubyLLM
49
52
  # @see RubyLLM::Agents::BaseAgent
50
53
  # @api public
51
54
  class Base < BaseAgent
52
- extend ModerationDSL
53
- include ModerationExecution
55
+ extend CallbacksDSL
56
+ include CallbacksExecution
54
57
 
55
58
  class << self
56
59
  # Returns the agent type for conversation agents
@@ -61,43 +64,27 @@ module RubyLLM
61
64
  end
62
65
  end
63
66
 
64
- # Execute the core LLM call with moderation support
67
+ # Execute the core LLM call with callback support
65
68
  #
66
- # This extends BaseAgent's execute method to add input and output
67
- # moderation checks when configured via the moderation DSL.
69
+ # This extends BaseAgent's execute method to add before/after
70
+ # callback hooks for custom preprocessing and postprocessing.
68
71
  #
69
72
  # @param context [Pipeline::Context] The execution context
70
73
  # @return [void] Sets context.output with the result
71
74
  def execute(context)
72
75
  @execution_started_at = context.started_at || Time.current
73
76
 
74
- # Input moderation check (before LLM call)
75
- if self.class.moderation_enabled? && should_moderate?(:input)
76
- input_text = build_moderation_input
77
- moderate_input(input_text)
77
+ # Run before_call callbacks
78
+ run_callbacks(:before, context)
78
79
 
79
- if moderation_blocked?
80
- context.output = build_moderation_blocked_result(:input)
81
- return
82
- end
83
- end
84
-
85
- # Execute the LLM call via parent
80
+ # Execute the LLM call
86
81
  client = build_client(context)
87
82
  response = execute_llm_call(client, context)
88
83
  capture_response(response, context)
89
84
  processed_content = process_response(response)
90
85
 
91
- # Output moderation check (after LLM call)
92
- if self.class.moderation_enabled? && should_moderate?(:output)
93
- output_text = processed_content.is_a?(String) ? processed_content : processed_content.to_s
94
- moderate_output(output_text)
95
-
96
- if moderation_blocked?
97
- context.output = build_moderation_blocked_result(:output)
98
- return
99
- end
100
- end
86
+ # Run after_call callbacks
87
+ run_callbacks(:after, context, response)
101
88
 
102
89
  context.output = build_result(processed_content, response, context)
103
90
  end
@@ -111,25 +98,6 @@ module RubyLLM
111
98
 
112
99
  tenant.is_a?(Hash) ? tenant[:id]&.to_s : nil
113
100
  end
114
-
115
- private
116
-
117
- # Check if execution model is available for moderation tracking
118
- #
119
- # @return [Boolean] true if Execution model can be used
120
- def execution_model_available?
121
- return @execution_model_available if defined?(@execution_model_available)
122
-
123
- @execution_model_available = begin
124
- RubyLLM::Agents::Execution.table_exists?
125
- rescue StandardError
126
- false
127
- end
128
- end
129
101
  end
130
102
  end
131
103
  end
132
-
133
- # Load moderation modules after class is defined (they reopen the class)
134
- require_relative "base/moderation_dsl"
135
- require_relative "base/moderation_execution"
@@ -167,15 +167,18 @@ module RubyLLM
167
167
  # enforcement: :soft
168
168
  # }
169
169
 
170
- # @!attribute [rw] alerts
171
- # Alert configuration for notifications.
172
- # @return [Hash, nil] Alert config with :slack_webhook_url, :webhook_url, :on_events, :custom keys
170
+ # @!attribute [rw] on_alert
171
+ # Alert handler proc called when governance events occur.
172
+ # Receives event name and payload hash. Filter events in your proc as needed.
173
+ # @return [Proc, nil] Alert handler or nil to disable (default: nil)
173
174
  # @example
174
- # config.alerts = {
175
- # slack_webhook_url: ENV["SLACK_WEBHOOK"],
176
- # webhook_url: ENV["AGENTS_WEBHOOK"],
177
- # on_events: [:budget_soft_cap, :budget_hard_cap, :breaker_open],
178
- # custom: ->(event, payload) { Rails.logger.info("Alert: #{event}") }
175
+ # config.on_alert = ->(event, payload) {
176
+ # case event
177
+ # when :budget_hard_cap
178
+ # Slack::Notifier.new(ENV["SLACK_WEBHOOK"]).ping("Budget exceeded")
179
+ # when :breaker_open
180
+ # PagerDuty.trigger(payload)
181
+ # end
179
182
  # }
180
183
 
181
184
  # @!attribute [rw] persist_prompts
@@ -188,17 +191,6 @@ module RubyLLM
188
191
  # Set to false to reduce storage or for privacy compliance.
189
192
  # @return [Boolean] Enable response persistence (default: true)
190
193
 
191
- # @!attribute [rw] redaction
192
- # Redaction configuration for PII and sensitive data.
193
- # @return [Hash, nil] Redaction config with :fields, :patterns, :placeholder, :max_value_length keys
194
- # @example
195
- # config.redaction = {
196
- # fields: %w[password api_key email ssn],
197
- # patterns: [/\b\d{3}-\d{2}-\d{4}\b/],
198
- # placeholder: "[REDACTED]",
199
- # max_value_length: 5000
200
- # }
201
-
202
194
  # @!attribute [rw] multi_tenancy_enabled
203
195
  # Whether multi-tenancy features are enabled.
204
196
  # When false, the gem behaves exactly as before (backward compatible).
@@ -274,35 +266,6 @@ module RubyLLM
274
266
  # @example
275
267
  # config.track_embeddings = false
276
268
 
277
- # @!attribute [rw] default_moderation_model
278
- # The default moderation model identifier for all agents.
279
- # Can be overridden per-agent using the `moderation` DSL method.
280
- # @return [String] Model identifier (default: "omni-moderation-latest")
281
- # @example
282
- # config.default_moderation_model = "text-moderation-007"
283
-
284
- # @!attribute [rw] default_moderation_threshold
285
- # The default threshold for moderation scores.
286
- # Content with scores at or above this threshold will be flagged.
287
- # Set to nil to use the provider's default flagging.
288
- # @return [Float, nil] Threshold (0.0-1.0) or nil for provider default (default: nil)
289
- # @example
290
- # config.default_moderation_threshold = 0.8
291
-
292
- # @!attribute [rw] default_moderation_action
293
- # The default action when content is flagged.
294
- # Can be overridden per-agent using the `moderation` DSL method.
295
- # @return [Symbol] Action (:block, :raise, :warn, :log) (default: :block)
296
- # @example
297
- # config.default_moderation_action = :raise
298
-
299
- # @!attribute [rw] track_moderation
300
- # Whether to track moderation executions in the database.
301
- # When enabled, moderation operations are logged as executions.
302
- # @return [Boolean] Enable moderation tracking (default: true)
303
- # @example
304
- # config.track_moderation = false
305
-
306
269
  # @!attribute [rw] default_transcription_model
307
270
  # The default transcription model identifier for all transcribers.
308
271
  # Can be overridden per-transcriber using the `model` DSL method.
@@ -376,6 +339,18 @@ module RubyLLM
376
339
  # @example
377
340
  # config.tool_result_max_length = 5000
378
341
 
342
+ # @!attribute [rw] redaction
343
+ # Configuration for PII and sensitive data redaction.
344
+ # When set, sensitive data is redacted before storing in execution records.
345
+ # @return [Hash, nil] Redaction config with :fields, :patterns, :placeholder, :max_value_length keys
346
+ # @example
347
+ # config.redaction = {
348
+ # fields: %w[ssn credit_card phone_number email],
349
+ # patterns: [/\b\d{3}-\d{2}-\d{4}\b/],
350
+ # placeholder: "[REDACTED]",
351
+ # max_value_length: 5000
352
+ # }
353
+
379
354
  # Attributes without validation (simple accessors)
380
355
  attr_accessor :default_model,
381
356
  :async_logging,
@@ -389,10 +364,9 @@ module RubyLLM
389
364
  :default_streaming,
390
365
  :default_tools,
391
366
  :default_thinking,
392
- :alerts,
367
+ :on_alert,
393
368
  :persist_prompts,
394
369
  :persist_responses,
395
- :redaction,
396
370
  :multi_tenancy_enabled,
397
371
  :persist_messages_summary,
398
372
  :default_retryable_patterns,
@@ -400,10 +374,6 @@ module RubyLLM
400
374
  :default_embedding_dimensions,
401
375
  :default_embedding_batch_size,
402
376
  :track_embeddings,
403
- :default_moderation_model,
404
- :default_moderation_threshold,
405
- :default_moderation_action,
406
- :track_moderation,
407
377
  :default_transcription_model,
408
378
  :track_transcriptions,
409
379
  :default_tts_provider,
@@ -437,7 +407,8 @@ module RubyLLM
437
407
  :default_background_output_format,
438
408
  :root_directory,
439
409
  :root_namespace,
440
- :tool_result_max_length
410
+ :tool_result_max_length,
411
+ :redaction
441
412
 
442
413
  # Attributes with validation (readers only, custom setters below)
443
414
  attr_reader :default_temperature,
@@ -634,10 +605,9 @@ module RubyLLM
634
605
 
635
606
  # Governance defaults
636
607
  @budgets = nil
637
- @alerts = nil
608
+ @on_alert = nil
638
609
  @persist_prompts = true
639
610
  @persist_responses = true
640
- @redaction = nil
641
611
 
642
612
  # Multi-tenancy defaults (disabled for backward compatibility)
643
613
  @multi_tenancy_enabled = false
@@ -654,12 +624,6 @@ module RubyLLM
654
624
  @default_embedding_batch_size = 100
655
625
  @track_embeddings = true
656
626
 
657
- # Moderation defaults
658
- @default_moderation_model = "omni-moderation-latest"
659
- @default_moderation_threshold = nil
660
- @default_moderation_action = :block
661
- @track_moderation = true
662
-
663
627
  # Transcription defaults
664
628
  @default_transcription_model = "whisper-1"
665
629
  @track_transcriptions = true
@@ -715,6 +679,9 @@ module RubyLLM
715
679
 
716
680
  # Tool tracking defaults
717
681
  @tool_result_max_length = 10_000
682
+
683
+ # Redaction defaults (disabled by default)
684
+ @redaction = nil
718
685
  end
719
686
 
720
687
  # Returns the configured cache store, falling back to Rails.cache
@@ -747,55 +714,6 @@ module RubyLLM
747
714
  default_retryable_patterns.values.flatten.uniq
748
715
  end
749
716
 
750
- # Returns whether alerts are configured
751
- #
752
- # @return [Boolean] true if any alert destination is configured
753
- def alerts_enabled?
754
- return false unless alerts.is_a?(Hash)
755
-
756
- alerts[:slack_webhook_url].present? ||
757
- alerts[:webhook_url].present? ||
758
- alerts[:custom].present? ||
759
- alerts[:email_recipients].present?
760
- end
761
-
762
- # Returns the list of events to alert on
763
- #
764
- # @return [Array<Symbol>] Event names to trigger alerts
765
- def alert_events
766
- alerts&.dig(:on_events) || []
767
- end
768
-
769
- # Returns merged redaction fields (default sensitive keys + configured)
770
- #
771
- # @return [Array<String>] Field names to redact
772
- def redaction_fields
773
- default_fields = %w[password token api_key secret credential auth key access_token]
774
- configured_fields = redaction&.dig(:fields) || []
775
- (default_fields + configured_fields).map(&:downcase).uniq
776
- end
777
-
778
- # Returns redaction patterns
779
- #
780
- # @return [Array<Regexp>] Patterns to match and redact
781
- def redaction_patterns
782
- redaction&.dig(:patterns) || []
783
- end
784
-
785
- # Returns the redaction placeholder string
786
- #
787
- # @return [String] Placeholder to replace redacted values
788
- def redaction_placeholder
789
- redaction&.dig(:placeholder) || "[REDACTED]"
790
- end
791
-
792
- # Returns the maximum value length before truncation
793
- #
794
- # @return [Integer, nil] Max length, or nil for no limit
795
- def redaction_max_value_length
796
- redaction&.dig(:max_value_length)
797
- end
798
-
799
717
  # Returns whether multi-tenancy is enabled
800
718
  #
801
719
  # @return [Boolean] true if multi-tenancy is enabled
@@ -869,8 +787,6 @@ module RubyLLM
869
787
  when :images then "images"
870
788
  when :audio then "audio"
871
789
  when :embedders then "embedders"
872
- when :moderators then "moderators"
873
- when :workflows then "workflows"
874
790
  when :text then "text"
875
791
  when :image then "image"
876
792
  end
@@ -895,16 +811,41 @@ module RubyLLM
895
811
 
896
812
  [
897
813
  base,
898
- "app/workflows", # Top-level workflows directory
899
814
  "#{base}/images",
900
815
  "#{base}/audio",
901
816
  "#{base}/embedders",
902
- "#{base}/moderators",
903
- "#{base}/workflows",
904
817
  "#{base}/tools"
905
818
  ]
906
819
  end
907
820
 
821
+ # Returns the redaction fields (parameter names to redact)
822
+ #
823
+ # @return [Array<String>] Fields to redact
824
+ def redaction_fields
825
+ redaction&.dig(:fields) || []
826
+ end
827
+
828
+ # Returns the redaction regex patterns
829
+ #
830
+ # @return [Array<Regexp>] Patterns to match and redact
831
+ def redaction_patterns
832
+ redaction&.dig(:patterns) || []
833
+ end
834
+
835
+ # Returns the redaction placeholder string
836
+ #
837
+ # @return [String] Placeholder for redacted values (default: "[REDACTED]")
838
+ def redaction_placeholder
839
+ redaction&.dig(:placeholder) || "[REDACTED]"
840
+ end
841
+
842
+ # Returns the max value length for redaction
843
+ #
844
+ # @return [Integer, nil] Max length before truncation, or nil for no limit
845
+ def redaction_max_value_length
846
+ redaction&.dig(:max_value_length)
847
+ end
848
+
908
849
  private
909
850
 
910
851
  # Validates that a value is within a range