ruby_llm-agents 0.5.0 → 1.0.0.beta.1

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 (190) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +189 -31
  3. data/app/controllers/ruby_llm/agents/agents_controller.rb +136 -16
  4. data/app/controllers/ruby_llm/agents/dashboard_controller.rb +29 -9
  5. data/app/controllers/ruby_llm/agents/workflows_controller.rb +355 -0
  6. data/app/helpers/ruby_llm/agents/application_helper.rb +25 -0
  7. data/app/models/ruby_llm/agents/execution.rb +3 -0
  8. data/app/models/ruby_llm/agents/tenant_budget.rb +58 -15
  9. data/app/services/ruby_llm/agents/agent_registry.rb +51 -12
  10. data/app/views/layouts/ruby_llm/agents/application.html.erb +2 -29
  11. data/app/views/ruby_llm/agents/agents/_agent.html.erb +13 -1
  12. data/app/views/ruby_llm/agents/agents/_config_agent.html.erb +235 -0
  13. data/app/views/ruby_llm/agents/agents/_config_embedder.html.erb +70 -0
  14. data/app/views/ruby_llm/agents/agents/_config_image_generator.html.erb +152 -0
  15. data/app/views/ruby_llm/agents/agents/_config_moderator.html.erb +63 -0
  16. data/app/views/ruby_llm/agents/agents/_config_speaker.html.erb +108 -0
  17. data/app/views/ruby_llm/agents/agents/_config_transcriber.html.erb +91 -0
  18. data/app/views/ruby_llm/agents/agents/_workflow.html.erb +1 -1
  19. data/app/views/ruby_llm/agents/agents/index.html.erb +74 -9
  20. data/app/views/ruby_llm/agents/agents/show.html.erb +18 -378
  21. data/app/views/ruby_llm/agents/dashboard/_agent_comparison.html.erb +269 -15
  22. data/app/views/ruby_llm/agents/executions/show.html.erb +16 -0
  23. data/app/views/ruby_llm/agents/shared/_agent_type_badge.html.erb +93 -0
  24. data/app/views/ruby_llm/agents/workflows/_step_performance.html.erb +236 -0
  25. data/app/views/ruby_llm/agents/workflows/_structure_parallel.html.erb +76 -0
  26. data/app/views/ruby_llm/agents/workflows/_structure_pipeline.html.erb +74 -0
  27. data/app/views/ruby_llm/agents/workflows/_structure_router.html.erb +108 -0
  28. data/app/views/ruby_llm/agents/workflows/show.html.erb +442 -0
  29. data/config/routes.rb +1 -0
  30. data/lib/generators/ruby_llm_agents/agent_generator.rb +56 -7
  31. data/lib/generators/ruby_llm_agents/background_remover_generator.rb +110 -0
  32. data/lib/generators/ruby_llm_agents/embedder_generator.rb +107 -0
  33. data/lib/generators/ruby_llm_agents/image_analyzer_generator.rb +115 -0
  34. data/lib/generators/ruby_llm_agents/image_editor_generator.rb +108 -0
  35. data/lib/generators/ruby_llm_agents/image_generator_generator.rb +116 -0
  36. data/lib/generators/ruby_llm_agents/image_pipeline_generator.rb +178 -0
  37. data/lib/generators/ruby_llm_agents/image_transformer_generator.rb +109 -0
  38. data/lib/generators/ruby_llm_agents/image_upscaler_generator.rb +103 -0
  39. data/lib/generators/ruby_llm_agents/image_variator_generator.rb +102 -0
  40. data/lib/generators/ruby_llm_agents/install_generator.rb +76 -4
  41. data/lib/generators/ruby_llm_agents/restructure_generator.rb +292 -0
  42. data/lib/generators/ruby_llm_agents/speaker_generator.rb +121 -0
  43. data/lib/generators/ruby_llm_agents/templates/add_execution_type_migration.rb.tt +8 -0
  44. data/lib/generators/ruby_llm_agents/templates/agent.rb.tt +99 -84
  45. data/lib/generators/ruby_llm_agents/templates/application_agent.rb.tt +42 -40
  46. data/lib/generators/ruby_llm_agents/templates/application_background_remover.rb.tt +26 -0
  47. data/lib/generators/ruby_llm_agents/templates/application_embedder.rb.tt +50 -0
  48. data/lib/generators/ruby_llm_agents/templates/application_image_analyzer.rb.tt +26 -0
  49. data/lib/generators/ruby_llm_agents/templates/application_image_editor.rb.tt +20 -0
  50. data/lib/generators/ruby_llm_agents/templates/application_image_generator.rb.tt +38 -0
  51. data/lib/generators/ruby_llm_agents/templates/application_image_pipeline.rb.tt +139 -0
  52. data/lib/generators/ruby_llm_agents/templates/application_image_transformer.rb.tt +21 -0
  53. data/lib/generators/ruby_llm_agents/templates/application_image_upscaler.rb.tt +20 -0
  54. data/lib/generators/ruby_llm_agents/templates/application_image_variator.rb.tt +20 -0
  55. data/lib/generators/ruby_llm_agents/templates/application_speaker.rb.tt +49 -0
  56. data/lib/generators/ruby_llm_agents/templates/application_transcriber.rb.tt +53 -0
  57. data/lib/generators/ruby_llm_agents/templates/background_remover.rb.tt +44 -0
  58. data/lib/generators/ruby_llm_agents/templates/embedder.rb.tt +41 -0
  59. data/lib/generators/ruby_llm_agents/templates/image_analyzer.rb.tt +45 -0
  60. data/lib/generators/ruby_llm_agents/templates/image_editor.rb.tt +35 -0
  61. data/lib/generators/ruby_llm_agents/templates/image_generator.rb.tt +47 -0
  62. data/lib/generators/ruby_llm_agents/templates/image_pipeline.rb.tt +50 -0
  63. data/lib/generators/ruby_llm_agents/templates/image_transformer.rb.tt +44 -0
  64. data/lib/generators/ruby_llm_agents/templates/image_upscaler.rb.tt +38 -0
  65. data/lib/generators/ruby_llm_agents/templates/image_variator.rb.tt +33 -0
  66. data/lib/generators/ruby_llm_agents/templates/skills/AGENTS.md.tt +228 -0
  67. data/lib/generators/ruby_llm_agents/templates/skills/BACKGROUND_REMOVERS.md.tt +131 -0
  68. data/lib/generators/ruby_llm_agents/templates/skills/EMBEDDERS.md.tt +255 -0
  69. data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_ANALYZERS.md.tt +120 -0
  70. data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_EDITORS.md.tt +102 -0
  71. data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_GENERATORS.md.tt +282 -0
  72. data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_PIPELINES.md.tt +228 -0
  73. data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_TRANSFORMERS.md.tt +120 -0
  74. data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_UPSCALERS.md.tt +110 -0
  75. data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_VARIATORS.md.tt +120 -0
  76. data/lib/generators/ruby_llm_agents/templates/skills/SPEAKERS.md.tt +212 -0
  77. data/lib/generators/ruby_llm_agents/templates/skills/TOOLS.md.tt +227 -0
  78. data/lib/generators/ruby_llm_agents/templates/skills/TRANSCRIBERS.md.tt +251 -0
  79. data/lib/generators/ruby_llm_agents/templates/skills/WORKFLOWS.md.tt +300 -0
  80. data/lib/generators/ruby_llm_agents/templates/speaker.rb.tt +56 -0
  81. data/lib/generators/ruby_llm_agents/templates/transcriber.rb.tt +51 -0
  82. data/lib/generators/ruby_llm_agents/transcriber_generator.rb +107 -0
  83. data/lib/generators/ruby_llm_agents/upgrade_generator.rb +152 -1
  84. data/lib/ruby_llm/agents/audio/speaker.rb +553 -0
  85. data/lib/ruby_llm/agents/audio/transcriber.rb +669 -0
  86. data/lib/ruby_llm/agents/base_agent.rb +675 -0
  87. data/lib/ruby_llm/agents/core/base/moderation_dsl.rb +181 -0
  88. data/lib/ruby_llm/agents/core/base/moderation_execution.rb +274 -0
  89. data/lib/ruby_llm/agents/core/base.rb +135 -0
  90. data/lib/ruby_llm/agents/core/configuration.rb +981 -0
  91. data/lib/ruby_llm/agents/core/errors.rb +150 -0
  92. data/lib/ruby_llm/agents/{instrumentation.rb → core/instrumentation.rb} +22 -1
  93. data/lib/ruby_llm/agents/core/llm_tenant.rb +358 -0
  94. data/lib/ruby_llm/agents/{version.rb → core/version.rb} +1 -1
  95. data/lib/ruby_llm/agents/dsl/base.rb +110 -0
  96. data/lib/ruby_llm/agents/dsl/caching.rb +142 -0
  97. data/lib/ruby_llm/agents/dsl/reliability.rb +307 -0
  98. data/lib/ruby_llm/agents/dsl.rb +41 -0
  99. data/lib/ruby_llm/agents/image/analyzer/dsl.rb +130 -0
  100. data/lib/ruby_llm/agents/image/analyzer/execution.rb +402 -0
  101. data/lib/ruby_llm/agents/image/analyzer.rb +90 -0
  102. data/lib/ruby_llm/agents/image/background_remover/dsl.rb +154 -0
  103. data/lib/ruby_llm/agents/image/background_remover/execution.rb +240 -0
  104. data/lib/ruby_llm/agents/image/background_remover.rb +89 -0
  105. data/lib/ruby_llm/agents/image/concerns/image_operation_dsl.rb +91 -0
  106. data/lib/ruby_llm/agents/image/concerns/image_operation_execution.rb +165 -0
  107. data/lib/ruby_llm/agents/image/editor/dsl.rb +56 -0
  108. data/lib/ruby_llm/agents/image/editor/execution.rb +207 -0
  109. data/lib/ruby_llm/agents/image/editor.rb +92 -0
  110. data/lib/ruby_llm/agents/image/generator/active_storage_support.rb +127 -0
  111. data/lib/ruby_llm/agents/image/generator/content_policy.rb +95 -0
  112. data/lib/ruby_llm/agents/image/generator/pricing.rb +353 -0
  113. data/lib/ruby_llm/agents/image/generator/templates.rb +124 -0
  114. data/lib/ruby_llm/agents/image/generator.rb +455 -0
  115. data/lib/ruby_llm/agents/image/pipeline/dsl.rb +213 -0
  116. data/lib/ruby_llm/agents/image/pipeline/execution.rb +382 -0
  117. data/lib/ruby_llm/agents/image/pipeline.rb +97 -0
  118. data/lib/ruby_llm/agents/image/transformer/dsl.rb +148 -0
  119. data/lib/ruby_llm/agents/image/transformer/execution.rb +223 -0
  120. data/lib/ruby_llm/agents/image/transformer.rb +95 -0
  121. data/lib/ruby_llm/agents/image/upscaler/dsl.rb +83 -0
  122. data/lib/ruby_llm/agents/image/upscaler/execution.rb +219 -0
  123. data/lib/ruby_llm/agents/image/upscaler.rb +81 -0
  124. data/lib/ruby_llm/agents/image/variator/dsl.rb +62 -0
  125. data/lib/ruby_llm/agents/image/variator/execution.rb +189 -0
  126. data/lib/ruby_llm/agents/image/variator.rb +80 -0
  127. data/lib/ruby_llm/agents/{alert_manager.rb → infrastructure/alert_manager.rb} +17 -22
  128. data/lib/ruby_llm/agents/infrastructure/budget/budget_query.rb +145 -0
  129. data/lib/ruby_llm/agents/infrastructure/budget/config_resolver.rb +149 -0
  130. data/lib/ruby_llm/agents/infrastructure/budget/forecaster.rb +68 -0
  131. data/lib/ruby_llm/agents/infrastructure/budget/spend_recorder.rb +279 -0
  132. data/lib/ruby_llm/agents/infrastructure/budget_tracker.rb +275 -0
  133. data/lib/ruby_llm/agents/{execution_logger_job.rb → infrastructure/execution_logger_job.rb} +17 -1
  134. data/lib/ruby_llm/agents/{reliability → infrastructure/reliability}/executor.rb +2 -1
  135. data/lib/ruby_llm/agents/{reliability → infrastructure/reliability}/retry_strategy.rb +9 -3
  136. data/lib/ruby_llm/agents/{reliability.rb → infrastructure/reliability.rb} +11 -21
  137. data/lib/ruby_llm/agents/pipeline/builder.rb +215 -0
  138. data/lib/ruby_llm/agents/pipeline/context.rb +255 -0
  139. data/lib/ruby_llm/agents/pipeline/executor.rb +86 -0
  140. data/lib/ruby_llm/agents/pipeline/middleware/base.rb +124 -0
  141. data/lib/ruby_llm/agents/pipeline/middleware/budget.rb +95 -0
  142. data/lib/ruby_llm/agents/pipeline/middleware/cache.rb +171 -0
  143. data/lib/ruby_llm/agents/pipeline/middleware/instrumentation.rb +415 -0
  144. data/lib/ruby_llm/agents/pipeline/middleware/reliability.rb +276 -0
  145. data/lib/ruby_llm/agents/pipeline/middleware/tenant.rb +196 -0
  146. data/lib/ruby_llm/agents/pipeline.rb +68 -0
  147. data/lib/ruby_llm/agents/{engine.rb → rails/engine.rb} +79 -11
  148. data/lib/ruby_llm/agents/results/background_removal_result.rb +286 -0
  149. data/lib/ruby_llm/agents/{result.rb → results/base.rb} +73 -1
  150. data/lib/ruby_llm/agents/results/embedding_result.rb +243 -0
  151. data/lib/ruby_llm/agents/results/image_analysis_result.rb +314 -0
  152. data/lib/ruby_llm/agents/results/image_edit_result.rb +250 -0
  153. data/lib/ruby_llm/agents/results/image_generation_result.rb +346 -0
  154. data/lib/ruby_llm/agents/results/image_pipeline_result.rb +399 -0
  155. data/lib/ruby_llm/agents/results/image_transform_result.rb +251 -0
  156. data/lib/ruby_llm/agents/results/image_upscale_result.rb +255 -0
  157. data/lib/ruby_llm/agents/results/image_variation_result.rb +237 -0
  158. data/lib/ruby_llm/agents/results/moderation_result.rb +158 -0
  159. data/lib/ruby_llm/agents/results/speech_result.rb +338 -0
  160. data/lib/ruby_llm/agents/results/transcription_result.rb +408 -0
  161. data/lib/ruby_llm/agents/text/embedder.rb +444 -0
  162. data/lib/ruby_llm/agents/text/moderator.rb +237 -0
  163. data/lib/ruby_llm/agents/workflow/async.rb +220 -0
  164. data/lib/ruby_llm/agents/workflow/async_executor.rb +156 -0
  165. data/lib/ruby_llm/agents/{workflow.rb → workflow/orchestrator.rb} +6 -5
  166. data/lib/ruby_llm/agents/workflow/parallel.rb +34 -17
  167. data/lib/ruby_llm/agents/workflow/thread_pool.rb +185 -0
  168. data/lib/ruby_llm/agents.rb +86 -20
  169. metadata +172 -34
  170. data/lib/ruby_llm/agents/base/caching.rb +0 -40
  171. data/lib/ruby_llm/agents/base/cost_calculation.rb +0 -105
  172. data/lib/ruby_llm/agents/base/dsl.rb +0 -324
  173. data/lib/ruby_llm/agents/base/execution.rb +0 -366
  174. data/lib/ruby_llm/agents/base/reliability_dsl.rb +0 -82
  175. data/lib/ruby_llm/agents/base/reliability_execution.rb +0 -136
  176. data/lib/ruby_llm/agents/base/response_building.rb +0 -86
  177. data/lib/ruby_llm/agents/base/tool_tracking.rb +0 -57
  178. data/lib/ruby_llm/agents/base.rb +0 -210
  179. data/lib/ruby_llm/agents/budget_tracker.rb +0 -733
  180. data/lib/ruby_llm/agents/configuration.rb +0 -394
  181. /data/lib/ruby_llm/agents/{deprecations.rb → core/deprecations.rb} +0 -0
  182. /data/lib/ruby_llm/agents/{inflections.rb → core/inflections.rb} +0 -0
  183. /data/lib/ruby_llm/agents/{resolved_config.rb → core/resolved_config.rb} +0 -0
  184. /data/lib/ruby_llm/agents/{attempt_tracker.rb → infrastructure/attempt_tracker.rb} +0 -0
  185. /data/lib/ruby_llm/agents/{cache_helper.rb → infrastructure/cache_helper.rb} +0 -0
  186. /data/lib/ruby_llm/agents/{circuit_breaker.rb → infrastructure/circuit_breaker.rb} +0 -0
  187. /data/lib/ruby_llm/agents/{redactor.rb → infrastructure/redactor.rb} +0 -0
  188. /data/lib/ruby_llm/agents/{reliability → infrastructure/reliability}/breaker_manager.rb +0 -0
  189. /data/lib/ruby_llm/agents/{reliability → infrastructure/reliability}/execution_constraints.rb +0 -0
  190. /data/lib/ruby_llm/agents/{reliability → infrastructure/reliability}/fallback_routing.rb +0 -0
@@ -0,0 +1,981 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module Agents
5
+ # Global configuration for RubyLLM::Agents
6
+ #
7
+ # Provides centralized settings for agent behavior, dashboard authentication,
8
+ # caching, and observability thresholds.
9
+ #
10
+ # @example Basic configuration
11
+ # RubyLLM::Agents.configure do |config|
12
+ # config.default_model = "gpt-4o"
13
+ # config.default_temperature = 0.7
14
+ # config.async_logging = true
15
+ # end
16
+ #
17
+ # @example Dashboard with HTTP Basic Auth
18
+ # RubyLLM::Agents.configure do |config|
19
+ # config.basic_auth_username = ENV["AGENTS_USER"]
20
+ # config.basic_auth_password = ENV["AGENTS_PASS"]
21
+ # end
22
+ #
23
+ # @example Dashboard with custom authentication
24
+ # RubyLLM::Agents.configure do |config|
25
+ # config.dashboard_parent_controller = "AdminController"
26
+ # config.dashboard_auth = ->(controller) { controller.current_user&.admin? }
27
+ # end
28
+ #
29
+ # @see RubyLLM::Agents.configure
30
+ # @api public
31
+ class Configuration
32
+ # @!attribute [rw] default_model
33
+ # The default LLM model identifier for all agents.
34
+ # Can be overridden per-agent using the `model` DSL method.
35
+ # @return [String] Model identifier (default: "gemini-2.0-flash")
36
+ # @example
37
+ # config.default_model = "gpt-4o"
38
+
39
+ # @!attribute [rw] default_temperature
40
+ # The default temperature for LLM responses (0.0 to 2.0).
41
+ # Lower values produce more deterministic outputs.
42
+ # @return [Float] Temperature value (default: 0.0)
43
+
44
+ # @!attribute [rw] default_timeout
45
+ # Maximum seconds to wait for an LLM response before timing out.
46
+ # @return [Integer] Timeout in seconds (default: 60)
47
+
48
+ # @!attribute [rw] async_logging
49
+ # Whether to log executions via background job (recommended for production).
50
+ # When false, executions are logged synchronously.
51
+ # @return [Boolean] Enable async logging (default: true)
52
+
53
+ # @!attribute [rw] retention_period
54
+ # How long to retain execution records before cleanup.
55
+ # @return [ActiveSupport::Duration] Retention period (default: 30.days)
56
+
57
+ # @!attribute [rw] anomaly_cost_threshold
58
+ # Cost threshold in dollars that triggers anomaly logging.
59
+ # Executions exceeding this cost are logged as warnings.
60
+ # @return [Float] Cost threshold in USD (default: 5.00)
61
+
62
+ # @!attribute [rw] anomaly_duration_threshold
63
+ # Duration threshold in milliseconds that triggers anomaly logging.
64
+ # @return [Integer] Duration threshold in ms (default: 10_000)
65
+
66
+ # @!attribute [rw] dashboard_auth
67
+ # Lambda for custom dashboard authentication.
68
+ # Receives the controller instance, should return truthy to allow access.
69
+ # @return [Proc] Authentication lambda (default: allows all)
70
+ # @example
71
+ # config.dashboard_auth = ->(c) { c.current_user&.admin? }
72
+
73
+ # @!attribute [rw] dashboard_parent_controller
74
+ # Parent controller class name for the dashboard.
75
+ # Use this to inherit authentication from your app's admin controller.
76
+ # @return [String] Controller class name (default: "ActionController::Base")
77
+
78
+ # @!attribute [rw] basic_auth_username
79
+ # Username for HTTP Basic Auth on the dashboard.
80
+ # Both username and password must be set to enable Basic Auth.
81
+ # @return [String, nil] Username or nil to disable (default: nil)
82
+
83
+ # @!attribute [rw] basic_auth_password
84
+ # Password for HTTP Basic Auth on the dashboard.
85
+ # @return [String, nil] Password or nil to disable (default: nil)
86
+
87
+ # @!attribute [rw] per_page
88
+ # Number of records per page in dashboard listings.
89
+ # @return [Integer] Records per page (default: 25)
90
+
91
+ # @!attribute [rw] recent_executions_limit
92
+ # Number of recent executions shown on the dashboard home.
93
+ # @return [Integer] Limit for recent executions (default: 10)
94
+
95
+ # @!attribute [rw] job_retry_attempts
96
+ # Number of retry attempts for the async logging job on failure.
97
+ # @return [Integer] Retry attempts (default: 3)
98
+
99
+ # @!attribute [w] cache_store
100
+ # Custom cache store for agent response caching.
101
+ # Falls back to Rails.cache if not set.
102
+ # @return [ActiveSupport::Cache::Store, nil]
103
+
104
+ # @!attribute [rw] default_retries
105
+ # Default retry configuration for all agents.
106
+ # Can be overridden per-agent using the `retries` DSL method.
107
+ # @return [Hash] Retry config with :max, :backoff, :base, :max_delay, :on keys
108
+ # @example
109
+ # config.default_retries = { max: 2, backoff: :exponential, base: 0.4, max_delay: 3.0, on: [] }
110
+
111
+ # @!attribute [rw] default_retryable_patterns
112
+ # Default patterns in error messages that indicate retryable errors.
113
+ # Organized by category for easy customization.
114
+ # @return [Hash<Symbol, Array<String>>] Categorized patterns
115
+ # @example
116
+ # config.default_retryable_patterns = {
117
+ # rate_limiting: ["rate limit", "429"],
118
+ # server_errors: ["500", "502", "503"],
119
+ # capacity: ["overloaded"]
120
+ # }
121
+
122
+ # @!attribute [rw] default_fallback_models
123
+ # Default fallback models for all agents.
124
+ # Can be overridden per-agent using the `fallback_models` DSL method.
125
+ # @return [Array<String>] List of model identifiers to try on failure
126
+
127
+ # @!attribute [rw] default_total_timeout
128
+ # Default total timeout across all retry attempts.
129
+ # Can be overridden per-agent using the `total_timeout` DSL method.
130
+ # @return [Integer, nil] Total timeout in seconds, or nil for no limit
131
+
132
+ # @!attribute [rw] default_streaming
133
+ # Whether streaming mode is enabled by default for all agents.
134
+ # When enabled and a block is passed to call, chunks are yielded as they arrive.
135
+ # Can be overridden per-agent using the `streaming` DSL method.
136
+ # @return [Boolean] Enable streaming (default: false)
137
+ # @example
138
+ # config.default_streaming = true
139
+
140
+ # @!attribute [rw] default_tools
141
+ # Default tools available to all agents.
142
+ # Should be an array of RubyLLM::Tool classes.
143
+ # Can be overridden or extended per-agent using the `tools` DSL method.
144
+ # @return [Array<Class>] Tool classes (default: [])
145
+ # @example
146
+ # config.default_tools = [WeatherTool, SearchTool]
147
+
148
+ # @!attribute [rw] default_thinking
149
+ # Default thinking/reasoning configuration for all agents.
150
+ # When set, enables extended thinking for supported models (Claude, Gemini, etc.).
151
+ # Can be overridden per-agent using the `thinking` DSL method.
152
+ # @return [Hash, nil] Thinking config with :effort and/or :budget keys (default: nil)
153
+ # @example Enable medium-effort thinking globally
154
+ # config.default_thinking = { effort: :medium }
155
+ # @example Enable high-effort thinking with budget
156
+ # config.default_thinking = { effort: :high, budget: 10000 }
157
+
158
+ # @!attribute [rw] budgets
159
+ # Budget configuration for cost governance.
160
+ # @return [Hash, nil] Budget config with :global_daily, :global_monthly, :per_agent_daily, :per_agent_monthly, :enforcement keys
161
+ # @example
162
+ # config.budgets = {
163
+ # global_daily: 25.0,
164
+ # global_monthly: 300.0,
165
+ # per_agent_daily: { "ContentAgent" => 5.0 },
166
+ # per_agent_monthly: { "ContentAgent" => 120.0 },
167
+ # enforcement: :soft
168
+ # }
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
173
+ # @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}") }
179
+ # }
180
+
181
+ # @!attribute [rw] persist_prompts
182
+ # Whether to persist system and user prompts in execution records.
183
+ # Set to false to reduce storage or for privacy compliance.
184
+ # @return [Boolean] Enable prompt persistence (default: true)
185
+
186
+ # @!attribute [rw] persist_responses
187
+ # Whether to persist LLM responses in execution records.
188
+ # Set to false to reduce storage or for privacy compliance.
189
+ # @return [Boolean] Enable response persistence (default: true)
190
+
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
+ # @!attribute [rw] multi_tenancy_enabled
203
+ # Whether multi-tenancy features are enabled.
204
+ # When false, the gem behaves exactly as before (backward compatible).
205
+ # @return [Boolean] Enable multi-tenancy (default: false)
206
+ # @example
207
+ # config.multi_tenancy_enabled = true
208
+
209
+ # @!attribute [rw] tenant_resolver
210
+ # Lambda that returns the current tenant identifier.
211
+ # Called whenever tenant context is needed for budget tracking,
212
+ # circuit breakers, and execution recording.
213
+ # @return [Proc] Tenant resolution lambda (default: -> { nil })
214
+ # @example Using Rails CurrentAttributes
215
+ # config.tenant_resolver = -> { Current.tenant&.id }
216
+ # @example Using request store
217
+ # config.tenant_resolver = -> { RequestStore[:tenant_id] }
218
+
219
+ # @!attribute [rw] tenant_config_resolver
220
+ # Lambda that returns tenant configuration without querying the database.
221
+ # Called when resolving tenant budget config. If set, this takes priority
222
+ # over the TenantBudget database lookup.
223
+ # @return [Proc, nil] Tenant config resolver lambda (default: nil)
224
+ # @example Using an external tenant service
225
+ # config.tenant_config_resolver = ->(tenant_id) {
226
+ # tenant = Tenant.find(tenant_id)
227
+ # {
228
+ # name: tenant.name,
229
+ # daily_limit: tenant.subscription.daily_budget,
230
+ # monthly_limit: tenant.subscription.monthly_budget,
231
+ # daily_token_limit: tenant.subscription.daily_tokens,
232
+ # monthly_token_limit: tenant.subscription.monthly_tokens,
233
+ # enforcement: tenant.subscription.hard_limits? ? :hard : :soft
234
+ # }
235
+ # }
236
+
237
+ # @!attribute [rw] persist_messages_summary
238
+ # Whether to persist a summary of conversation messages in execution records.
239
+ # When true, stores message count and first/last messages (truncated).
240
+ # Set to false to disable message summary persistence.
241
+ # @return [Boolean] Enable messages summary persistence (default: true)
242
+
243
+ # @!attribute [rw] messages_summary_max_length
244
+ # Maximum character length for message content in the summary.
245
+ # Content exceeding this length will be truncated with "...".
246
+ # @return [Integer] Max length for message content (default: 500)
247
+
248
+ # @!attribute [rw] default_embedding_model
249
+ # The default embedding model identifier for all embedders.
250
+ # Can be overridden per-embedder using the `model` DSL method.
251
+ # @return [String] Model identifier (default: "text-embedding-3-small")
252
+ # @example
253
+ # config.default_embedding_model = "text-embedding-3-large"
254
+
255
+ # @!attribute [rw] default_embedding_dimensions
256
+ # The default vector dimensions for embeddings.
257
+ # Set to nil to use the model's default dimensions.
258
+ # Some models (like OpenAI text-embedding-3) support dimension reduction.
259
+ # @return [Integer, nil] Dimensions or nil for model default (default: nil)
260
+ # @example
261
+ # config.default_embedding_dimensions = 512
262
+
263
+ # @!attribute [rw] default_embedding_batch_size
264
+ # The default batch size for embedding operations.
265
+ # When embedding multiple texts, they are split into batches of this size.
266
+ # @return [Integer] Batch size (default: 100)
267
+ # @example
268
+ # config.default_embedding_batch_size = 50
269
+
270
+ # @!attribute [rw] track_embeddings
271
+ # Whether to track embedding executions in the database.
272
+ # When enabled, embedding operations are logged like agent executions.
273
+ # @return [Boolean] Enable embedding tracking (default: true)
274
+ # @example
275
+ # config.track_embeddings = false
276
+
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
+ # @!attribute [rw] default_transcription_model
307
+ # The default transcription model identifier for all transcribers.
308
+ # Can be overridden per-transcriber using the `model` DSL method.
309
+ # @return [String] Model identifier (default: "whisper-1")
310
+ # @example
311
+ # config.default_transcription_model = "gpt-4o-transcribe"
312
+
313
+ # @!attribute [rw] track_transcriptions
314
+ # Whether to track transcription executions in the database.
315
+ # When enabled, transcription operations are logged as executions.
316
+ # @return [Boolean] Enable transcription tracking (default: true)
317
+ # @example
318
+ # config.track_transcriptions = false
319
+
320
+ # @!attribute [rw] default_tts_provider
321
+ # The default TTS provider for all speakers.
322
+ # Can be overridden per-speaker using the `provider` DSL method.
323
+ # @return [Symbol] Provider (:openai, :elevenlabs, :google, :polly) (default: :openai)
324
+ # @example
325
+ # config.default_tts_provider = :elevenlabs
326
+
327
+ # @!attribute [rw] default_tts_model
328
+ # The default TTS model identifier for all speakers.
329
+ # Can be overridden per-speaker using the `model` DSL method.
330
+ # @return [String] Model identifier (default: "tts-1")
331
+ # @example
332
+ # config.default_tts_model = "tts-1-hd"
333
+
334
+ # @!attribute [rw] default_tts_voice
335
+ # The default voice for all speakers.
336
+ # Can be overridden per-speaker using the `voice` DSL method.
337
+ # @return [String] Voice name (default: "nova")
338
+ # @example
339
+ # config.default_tts_voice = "alloy"
340
+
341
+ # @!attribute [rw] track_speech
342
+ # Whether to track speech executions in the database.
343
+ # When enabled, speech operations are logged as executions.
344
+ # @return [Boolean] Enable speech tracking (default: true)
345
+ # @example
346
+ # config.track_speech = false
347
+
348
+ # @!attribute [rw] async_max_concurrency
349
+ # Maximum number of concurrent async operations when using batch processing.
350
+ # Controls the semaphore limit for Async::Semaphore.
351
+ # @return [Integer] Max concurrent operations (default: 10)
352
+ # @example
353
+ # config.async_max_concurrency = 20
354
+
355
+ # @!attribute [rw] root_directory
356
+ # The root directory name under app/ for all LLM components.
357
+ # This allows customization of the directory structure.
358
+ # @return [String] Directory name (default: "llm")
359
+ # @example
360
+ # config.root_directory = "ai" # Creates app/ai/ instead of app/llm/
361
+
362
+ # @!attribute [rw] root_namespace
363
+ # The root namespace for all LLM component classes.
364
+ # This should match the root_directory in camelized form.
365
+ # Set to nil or "" to use no namespace (classes at top-level).
366
+ # @return [String, nil] Namespace (default: "Llm")
367
+ # @example Custom namespace
368
+ # config.root_namespace = "AI" # Uses AI:: instead of Llm::
369
+ # @example No namespace (top-level classes)
370
+ # config.root_namespace = nil # Uses top-level classes (ApplicationAgent, etc.)
371
+
372
+ # Attributes without validation (simple accessors)
373
+ attr_accessor :default_model,
374
+ :async_logging,
375
+ :async_max_concurrency,
376
+ :retention_period,
377
+ :dashboard_parent_controller,
378
+ :basic_auth_username,
379
+ :basic_auth_password,
380
+ :default_fallback_models,
381
+ :default_total_timeout,
382
+ :default_streaming,
383
+ :default_tools,
384
+ :default_thinking,
385
+ :alerts,
386
+ :persist_prompts,
387
+ :persist_responses,
388
+ :redaction,
389
+ :multi_tenancy_enabled,
390
+ :persist_messages_summary,
391
+ :default_retryable_patterns,
392
+ :default_embedding_model,
393
+ :default_embedding_dimensions,
394
+ :default_embedding_batch_size,
395
+ :track_embeddings,
396
+ :default_moderation_model,
397
+ :default_moderation_threshold,
398
+ :default_moderation_action,
399
+ :track_moderation,
400
+ :default_transcription_model,
401
+ :track_transcriptions,
402
+ :default_tts_provider,
403
+ :default_tts_model,
404
+ :default_tts_voice,
405
+ :track_speech,
406
+ :track_executions,
407
+ :track_audio,
408
+ :track_cache_hits,
409
+ :default_image_model,
410
+ :default_image_size,
411
+ :default_image_quality,
412
+ :default_image_style,
413
+ :max_image_prompt_length,
414
+ :track_image_generation,
415
+ :image_model_aliases,
416
+ :litellm_pricing_url,
417
+ :litellm_pricing_cache_ttl,
418
+ :default_image_cost,
419
+ :image_model_pricing,
420
+ :default_variator_model,
421
+ :default_editor_model,
422
+ :default_transformer_model,
423
+ :default_upscaler_model,
424
+ :default_variation_strength,
425
+ :default_transform_strength,
426
+ :default_analyzer_model,
427
+ :default_analysis_type,
428
+ :default_analyzer_max_tags,
429
+ :default_background_remover_model,
430
+ :default_background_output_format,
431
+ :root_directory,
432
+ :root_namespace
433
+
434
+ # Attributes with validation (readers only, custom setters below)
435
+ attr_reader :default_temperature,
436
+ :default_timeout,
437
+ :anomaly_cost_threshold,
438
+ :anomaly_duration_threshold,
439
+ :per_page,
440
+ :recent_executions_limit,
441
+ :job_retry_attempts,
442
+ :messages_summary_max_length,
443
+ :dashboard_auth,
444
+ :tenant_resolver,
445
+ :tenant_config_resolver,
446
+ :default_retries,
447
+ :budgets
448
+
449
+ attr_writer :cache_store
450
+
451
+ # Sets default_temperature with validation
452
+ #
453
+ # @param value [Float] Temperature (0.0 to 2.0)
454
+ # @raise [ArgumentError] If value is outside valid range
455
+ def default_temperature=(value)
456
+ validate_range!(:default_temperature, value, 0.0, 2.0)
457
+ @default_temperature = value
458
+ end
459
+
460
+ # Sets default_timeout with validation
461
+ #
462
+ # @param value [Integer] Timeout in seconds (must be > 0)
463
+ # @raise [ArgumentError] If value is not positive
464
+ def default_timeout=(value)
465
+ validate_positive!(:default_timeout, value)
466
+ @default_timeout = value
467
+ end
468
+
469
+ # Sets anomaly_cost_threshold with validation
470
+ #
471
+ # @param value [Float] Cost threshold (must be >= 0)
472
+ # @raise [ArgumentError] If value is negative
473
+ def anomaly_cost_threshold=(value)
474
+ validate_non_negative!(:anomaly_cost_threshold, value)
475
+ @anomaly_cost_threshold = value
476
+ end
477
+
478
+ # Sets anomaly_duration_threshold with validation
479
+ #
480
+ # @param value [Integer] Duration threshold in ms (must be >= 0)
481
+ # @raise [ArgumentError] If value is negative
482
+ def anomaly_duration_threshold=(value)
483
+ validate_non_negative!(:anomaly_duration_threshold, value)
484
+ @anomaly_duration_threshold = value
485
+ end
486
+
487
+ # Sets per_page with validation
488
+ #
489
+ # @param value [Integer] Records per page (must be > 0)
490
+ # @raise [ArgumentError] If value is not positive
491
+ def per_page=(value)
492
+ validate_positive!(:per_page, value)
493
+ @per_page = value
494
+ end
495
+
496
+ # Sets recent_executions_limit with validation
497
+ #
498
+ # @param value [Integer] Limit (must be > 0)
499
+ # @raise [ArgumentError] If value is not positive
500
+ def recent_executions_limit=(value)
501
+ validate_positive!(:recent_executions_limit, value)
502
+ @recent_executions_limit = value
503
+ end
504
+
505
+ # Sets job_retry_attempts with validation
506
+ #
507
+ # @param value [Integer] Retry attempts (must be >= 0)
508
+ # @raise [ArgumentError] If value is negative
509
+ def job_retry_attempts=(value)
510
+ validate_non_negative!(:job_retry_attempts, value)
511
+ @job_retry_attempts = value
512
+ end
513
+
514
+ # Sets messages_summary_max_length with validation
515
+ #
516
+ # @param value [Integer] Max length (must be > 0)
517
+ # @raise [ArgumentError] If value is not positive
518
+ def messages_summary_max_length=(value)
519
+ validate_positive!(:messages_summary_max_length, value)
520
+ @messages_summary_max_length = value
521
+ end
522
+
523
+ # Sets dashboard_auth with validation
524
+ #
525
+ # @param value [Proc, nil] Authentication lambda or nil
526
+ # @raise [ArgumentError] If value is not callable or nil
527
+ def dashboard_auth=(value)
528
+ validate_callable!(:dashboard_auth, value, allow_nil: true)
529
+ @dashboard_auth = value
530
+ end
531
+
532
+ # Sets tenant_resolver with validation
533
+ #
534
+ # @param value [Proc] Tenant resolution lambda (must be callable)
535
+ # @raise [ArgumentError] If value is not callable
536
+ def tenant_resolver=(value)
537
+ validate_callable!(:tenant_resolver, value, allow_nil: false)
538
+ @tenant_resolver = value
539
+ end
540
+
541
+ # Sets tenant_config_resolver with validation
542
+ #
543
+ # @param value [Proc, nil] Tenant config resolver lambda or nil
544
+ # @raise [ArgumentError] If value is not callable or nil
545
+ def tenant_config_resolver=(value)
546
+ validate_callable!(:tenant_config_resolver, value, allow_nil: true)
547
+ @tenant_config_resolver = value
548
+ end
549
+
550
+ # Sets default_retries with validation
551
+ #
552
+ # @param value [Hash] Retry configuration
553
+ # @raise [ArgumentError] If any values are invalid
554
+ def default_retries=(value)
555
+ validate_retries!(value)
556
+ @default_retries = value
557
+ end
558
+
559
+ # Sets budgets with validation
560
+ #
561
+ # @param value [Hash, nil] Budget configuration
562
+ # @raise [ArgumentError] If enforcement is invalid
563
+ def budgets=(value)
564
+ validate_budgets!(value)
565
+ @budgets = value
566
+ end
567
+
568
+ # Sets default_embedding_batch_size with validation
569
+ #
570
+ # @param value [Integer] Batch size (must be > 0)
571
+ # @raise [ArgumentError] If value is not positive
572
+ def default_embedding_batch_size=(value)
573
+ validate_positive!(:default_embedding_batch_size, value)
574
+ @default_embedding_batch_size = value
575
+ end
576
+
577
+ # Sets default_embedding_dimensions with validation
578
+ #
579
+ # @param value [Integer, nil] Dimensions (must be nil or > 0)
580
+ # @raise [ArgumentError] If value is not nil or positive
581
+ def default_embedding_dimensions=(value)
582
+ unless value.nil? || (value.is_a?(Numeric) && value > 0)
583
+ raise ArgumentError, "default_embedding_dimensions must be nil or greater than 0"
584
+ end
585
+
586
+ @default_embedding_dimensions = value
587
+ end
588
+
589
+ # Initializes configuration with default values
590
+ #
591
+ # @return [Configuration] A new configuration instance with defaults
592
+ # @api private
593
+ def initialize
594
+ @default_model = "gemini-2.0-flash"
595
+ @default_temperature = 0.0
596
+ @default_timeout = 60
597
+ @cache_store = nil
598
+ @async_logging = true
599
+ @async_max_concurrency = 10
600
+ @retention_period = 30.days
601
+ @anomaly_cost_threshold = 5.00
602
+ @anomaly_duration_threshold = 10_000
603
+ @dashboard_auth = ->(_controller) { true }
604
+ @dashboard_parent_controller = "ActionController::Base"
605
+ @basic_auth_username = nil
606
+ @basic_auth_password = nil
607
+ @per_page = 25
608
+ @recent_executions_limit = 10
609
+ @job_retry_attempts = 3
610
+
611
+ # Reliability defaults (all disabled by default for backward compatibility)
612
+ @default_retries = { max: 0, backoff: :exponential, base: 0.4, max_delay: 3.0, on: [] }
613
+ @default_fallback_models = []
614
+ @default_total_timeout = nil
615
+ @default_retryable_patterns = {
616
+ rate_limiting: ["rate limit", "rate_limit", "too many requests", "429"],
617
+ server_errors: ["500", "502", "503", "504", "service unavailable",
618
+ "internal server error", "bad gateway", "gateway timeout"],
619
+ capacity: ["overloaded", "capacity"]
620
+ }
621
+
622
+ # Streaming, tools, and thinking defaults
623
+ @default_streaming = false
624
+ @default_tools = []
625
+ @default_thinking = nil
626
+
627
+ # Governance defaults
628
+ @budgets = nil
629
+ @alerts = nil
630
+ @persist_prompts = true
631
+ @persist_responses = true
632
+ @redaction = nil
633
+
634
+ # Multi-tenancy defaults (disabled for backward compatibility)
635
+ @multi_tenancy_enabled = false
636
+ @tenant_resolver = -> { nil }
637
+ @tenant_config_resolver = nil
638
+
639
+ # Messages summary defaults
640
+ @persist_messages_summary = true
641
+ @messages_summary_max_length = 500
642
+
643
+ # Embedding defaults
644
+ @default_embedding_model = "text-embedding-3-small"
645
+ @default_embedding_dimensions = nil
646
+ @default_embedding_batch_size = 100
647
+ @track_embeddings = true
648
+
649
+ # Moderation defaults
650
+ @default_moderation_model = "omni-moderation-latest"
651
+ @default_moderation_threshold = nil
652
+ @default_moderation_action = :block
653
+ @track_moderation = true
654
+
655
+ # Transcription defaults
656
+ @default_transcription_model = "whisper-1"
657
+ @track_transcriptions = true
658
+
659
+ # TTS/Speech defaults
660
+ @default_tts_provider = :openai
661
+ @default_tts_model = "tts-1"
662
+ @default_tts_voice = "nova"
663
+ @track_speech = true
664
+
665
+ # Execution/conversation agent tracking
666
+ @track_executions = true
667
+ @track_audio = true
668
+ @track_cache_hits = true
669
+
670
+ # Image Generation defaults
671
+ @default_image_model = "gpt-image-1"
672
+ @default_image_size = "1024x1024"
673
+ @default_image_quality = "standard"
674
+ @default_image_style = "vivid"
675
+ @max_image_prompt_length = 4000
676
+ @track_image_generation = true
677
+ @image_model_aliases = {
678
+ fast: "flux-schnell",
679
+ quality: "gpt-image-1",
680
+ cheap: "sdxl"
681
+ }
682
+
683
+ # Image Pricing defaults
684
+ @litellm_pricing_url = nil # Use default from Pricing module
685
+ @litellm_pricing_cache_ttl = nil # Use default (24 hours)
686
+ @default_image_cost = 0.04 # Fallback cost per image
687
+ @image_model_pricing = {} # User-defined pricing overrides
688
+
689
+ # Phase 2: Image Variation, Editing, Transformation, Upscaling defaults
690
+ @default_variator_model = nil # Falls back to default_image_model
691
+ @default_editor_model = nil # Falls back to default_image_model
692
+ @default_transformer_model = "sdxl" # SDXL is good for transformations
693
+ @default_upscaler_model = "real-esrgan" # Real-ESRGAN for upscaling
694
+ @default_variation_strength = 0.5 # How different variations should be
695
+ @default_transform_strength = 0.75 # How much to transform
696
+
697
+ # Phase 3: Image Analysis and Background Removal defaults
698
+ @default_analyzer_model = "gpt-4o" # Vision model for analysis
699
+ @default_analysis_type = :detailed # Default analysis type
700
+ @default_analyzer_max_tags = 10 # Max tags to extract
701
+ @default_background_remover_model = "rembg" # Default remover model
702
+ @default_background_output_format = :png # Output format for transparency
703
+
704
+ # Directory structure defaults
705
+ @root_directory = "llm" # Root directory under app/
706
+ @root_namespace = "Llm" # Root namespace for classes
707
+ end
708
+
709
+ # Returns the configured cache store, falling back to Rails.cache
710
+ #
711
+ # @return [ActiveSupport::Cache::Store] The cache store instance
712
+ # @example Using a custom cache store
713
+ # config.cache_store = ActiveSupport::Cache::MemoryStore.new
714
+ def cache_store
715
+ @cache_store || Rails.cache
716
+ end
717
+
718
+ # Returns whether budgets are configured and enforcement is enabled
719
+ #
720
+ # @return [Boolean] true if budgets are configured with enforcement
721
+ def budgets_enabled?
722
+ budgets.is_a?(Hash) && budgets[:enforcement] && budgets[:enforcement] != :none
723
+ end
724
+
725
+ # Returns the budget enforcement mode
726
+ #
727
+ # @return [Symbol] :none, :soft, or :hard
728
+ def budget_enforcement
729
+ budgets&.dig(:enforcement) || :none
730
+ end
731
+
732
+ # Returns all retryable patterns as a flat array
733
+ #
734
+ # @return [Array<String>] All patterns from all categories
735
+ def all_retryable_patterns
736
+ default_retryable_patterns.values.flatten.uniq
737
+ end
738
+
739
+ # Returns whether alerts are configured
740
+ #
741
+ # @return [Boolean] true if any alert destination is configured
742
+ def alerts_enabled?
743
+ return false unless alerts.is_a?(Hash)
744
+
745
+ alerts[:slack_webhook_url].present? ||
746
+ alerts[:webhook_url].present? ||
747
+ alerts[:custom].present?
748
+ end
749
+
750
+ # Returns the list of events to alert on
751
+ #
752
+ # @return [Array<Symbol>] Event names to trigger alerts
753
+ def alert_events
754
+ alerts&.dig(:on_events) || []
755
+ end
756
+
757
+ # Returns merged redaction fields (default sensitive keys + configured)
758
+ #
759
+ # @return [Array<String>] Field names to redact
760
+ def redaction_fields
761
+ default_fields = %w[password token api_key secret credential auth key access_token]
762
+ configured_fields = redaction&.dig(:fields) || []
763
+ (default_fields + configured_fields).map(&:downcase).uniq
764
+ end
765
+
766
+ # Returns redaction patterns
767
+ #
768
+ # @return [Array<Regexp>] Patterns to match and redact
769
+ def redaction_patterns
770
+ redaction&.dig(:patterns) || []
771
+ end
772
+
773
+ # Returns the redaction placeholder string
774
+ #
775
+ # @return [String] Placeholder to replace redacted values
776
+ def redaction_placeholder
777
+ redaction&.dig(:placeholder) || "[REDACTED]"
778
+ end
779
+
780
+ # Returns the maximum value length before truncation
781
+ #
782
+ # @return [Integer, nil] Max length, or nil for no limit
783
+ def redaction_max_value_length
784
+ redaction&.dig(:max_value_length)
785
+ end
786
+
787
+ # Returns whether multi-tenancy is enabled
788
+ #
789
+ # @return [Boolean] true if multi-tenancy is enabled
790
+ def multi_tenancy_enabled?
791
+ @multi_tenancy_enabled == true
792
+ end
793
+
794
+ # Returns the current tenant ID from the resolver
795
+ #
796
+ # @return [String, nil] Current tenant identifier or nil
797
+ def current_tenant_id
798
+ return nil unless multi_tenancy_enabled?
799
+
800
+ tenant_resolver&.call
801
+ end
802
+
803
+ # Returns whether the async gem is available
804
+ #
805
+ # @return [Boolean] true if async gem is loaded
806
+ def async_available?
807
+ defined?(::Async) && defined?(::Async::Semaphore)
808
+ end
809
+
810
+ # Returns whether we're currently inside an async context
811
+ #
812
+ # @return [Boolean] true if running in a fiber with async scheduler
813
+ def async_context?
814
+ return false unless async_available?
815
+
816
+ defined?(::Async::Task) && ::Async::Task.current?
817
+ rescue StandardError
818
+ false
819
+ end
820
+
821
+ # Returns the full namespace for a given category
822
+ #
823
+ # @param category [Symbol, nil] Category (:audio, :image, :text, or nil for root)
824
+ # @return [String, nil] Full namespace string, or nil if no namespace configured
825
+ # @example With default namespace
826
+ # namespace_for(:image) #=> "Llm::Image"
827
+ # namespace_for(nil) #=> "Llm"
828
+ # @example With no namespace (root_namespace = nil)
829
+ # namespace_for(:image) #=> "Image"
830
+ # namespace_for(nil) #=> nil
831
+ def namespace_for(category = nil)
832
+ # Handle no namespace configuration
833
+ if root_namespace.blank?
834
+ case category
835
+ when :audio then "Audio"
836
+ when :image then "Image"
837
+ when :text then "Text"
838
+ else nil
839
+ end
840
+ else
841
+ case category
842
+ when :audio then "#{root_namespace}::Audio"
843
+ when :image then "#{root_namespace}::Image"
844
+ when :text then "#{root_namespace}::Text"
845
+ else root_namespace
846
+ end
847
+ end
848
+ end
849
+
850
+ # Returns the full path under app/ for a given category and type
851
+ #
852
+ # @param category [Symbol, nil] Category (:audio, :image, :text, or nil)
853
+ # @param type [String] Component type (e.g., "agents", "speakers", "generators")
854
+ # @return [String] Full path relative to Rails.root
855
+ # @example
856
+ # path_for(:image, "generators") #=> "app/llm/image/generators"
857
+ # path_for(nil, "agents") #=> "app/llm/agents"
858
+ def path_for(category, type)
859
+ case category
860
+ when :audio then "app/#{root_directory}/audio/#{type}"
861
+ when :image then "app/#{root_directory}/image/#{type}"
862
+ when :text then "app/#{root_directory}/text/#{type}"
863
+ else "app/#{root_directory}/#{type}"
864
+ end
865
+ end
866
+
867
+ # Returns all autoload paths for LLM components
868
+ #
869
+ # @return [Array<String>] List of paths relative to Rails.root
870
+ def all_autoload_paths
871
+ [
872
+ # Top-level
873
+ path_for(nil, "agents"),
874
+ path_for(nil, "workflows"),
875
+ path_for(nil, "tools"),
876
+ # Audio
877
+ path_for(:audio, "speakers"),
878
+ path_for(:audio, "transcribers"),
879
+ # Image
880
+ path_for(:image, "analyzers"),
881
+ path_for(:image, "background_removers"),
882
+ path_for(:image, "editors"),
883
+ path_for(:image, "generators"),
884
+ path_for(:image, "transformers"),
885
+ path_for(:image, "upscalers"),
886
+ path_for(:image, "variators"),
887
+ # Text
888
+ path_for(:text, "embedders"),
889
+ path_for(:text, "moderators")
890
+ ]
891
+ end
892
+
893
+ private
894
+
895
+ # Validates that a value is within a range
896
+ #
897
+ # @param attr [Symbol] Attribute name for error message
898
+ # @param value [Numeric] Value to validate
899
+ # @param min [Numeric] Minimum value (inclusive)
900
+ # @param max [Numeric] Maximum value (inclusive)
901
+ # @raise [ArgumentError] If value is outside range
902
+ def validate_range!(attr, value, min, max)
903
+ return if value.is_a?(Numeric) && value >= min && value <= max
904
+
905
+ raise ArgumentError, "#{attr} must be between #{min} and #{max}"
906
+ end
907
+
908
+ # Validates that a value is positive (greater than 0)
909
+ #
910
+ # @param attr [Symbol] Attribute name for error message
911
+ # @param value [Numeric] Value to validate
912
+ # @raise [ArgumentError] If value is not positive
913
+ def validate_positive!(attr, value)
914
+ return if value.is_a?(Numeric) && value > 0
915
+
916
+ raise ArgumentError, "#{attr} must be greater than 0"
917
+ end
918
+
919
+ # Validates that a value is non-negative (>= 0)
920
+ #
921
+ # @param attr [Symbol] Attribute name for error message
922
+ # @param value [Numeric] Value to validate
923
+ # @raise [ArgumentError] If value is negative
924
+ def validate_non_negative!(attr, value)
925
+ return if value.is_a?(Numeric) && value >= 0
926
+
927
+ raise ArgumentError, "#{attr} must be >= 0"
928
+ end
929
+
930
+ # Validates that a value is callable (responds to :call)
931
+ #
932
+ # @param attr [Symbol] Attribute name for error message
933
+ # @param value [Object] Value to validate
934
+ # @param allow_nil [Boolean] Whether nil is allowed
935
+ # @raise [ArgumentError] If value is not callable (or nil when allowed)
936
+ def validate_callable!(attr, value, allow_nil:)
937
+ return if allow_nil && value.nil?
938
+ return if value.respond_to?(:call)
939
+
940
+ if allow_nil
941
+ raise ArgumentError, "#{attr} must be callable or nil"
942
+ else
943
+ raise ArgumentError, "#{attr} must be callable"
944
+ end
945
+ end
946
+
947
+ # Validates retries configuration hash
948
+ #
949
+ # @param value [Hash] Retries configuration
950
+ # @raise [ArgumentError] If any values are invalid
951
+ def validate_retries!(value)
952
+ return unless value.is_a?(Hash)
953
+
954
+ if value.key?(:backoff) && ![:exponential, :constant].include?(value[:backoff])
955
+ raise ArgumentError, "default_retries[:backoff] must be :exponential or :constant"
956
+ end
957
+
958
+ if value.key?(:base) && (!value[:base].is_a?(Numeric) || value[:base] <= 0)
959
+ raise ArgumentError, "default_retries[:base] must be greater than 0"
960
+ end
961
+
962
+ if value.key?(:max_delay) && (!value[:max_delay].is_a?(Numeric) || value[:max_delay] <= 0)
963
+ raise ArgumentError, "default_retries[:max_delay] must be greater than 0"
964
+ end
965
+ end
966
+
967
+ # Validates budgets configuration hash
968
+ #
969
+ # @param value [Hash, nil] Budgets configuration
970
+ # @raise [ArgumentError] If enforcement is invalid
971
+ def validate_budgets!(value)
972
+ return if value.nil?
973
+ return unless value.is_a?(Hash)
974
+
975
+ if value.key?(:enforcement) && ![:none, :soft, :hard].include?(value[:enforcement])
976
+ raise ArgumentError, "budgets[:enforcement] must be :none, :soft, or :hard"
977
+ end
978
+ end
979
+ end
980
+ end
981
+ end