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,274 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module RubyLLM
4
- module Agents
5
- # Execution logic for content moderation
6
- #
7
- # Provides methods to check content against moderation policies
8
- # and handle flagged content according to configuration.
9
- #
10
- # @api private
11
- module ModerationExecution
12
- # Moderates input text if input moderation is enabled
13
- #
14
- # @param text [String] Text to moderate
15
- # @return [RubyLLM::Moderation, nil] Moderation result or nil if not enabled
16
- def moderate_input(text)
17
- return nil unless should_moderate?(:input)
18
-
19
- perform_moderation(text, :input)
20
- end
21
-
22
- # Moderates output text if output moderation is enabled
23
- #
24
- # @param text [String] Text to moderate
25
- # @return [RubyLLM::Moderation, nil] Moderation result or nil if not enabled
26
- def moderate_output(text)
27
- return nil unless should_moderate?(:output)
28
-
29
- perform_moderation(text, :output)
30
- end
31
-
32
- # Returns whether moderation was blocked
33
- #
34
- # @return [Boolean] true if content was blocked by moderation
35
- def moderation_blocked?
36
- @moderation_blocked == true
37
- end
38
-
39
- # Returns the phase where moderation blocked content
40
- #
41
- # @return [Symbol, nil] :input or :output, or nil if not blocked
42
- def moderation_blocked_phase
43
- @moderation_blocked_phase
44
- end
45
-
46
- # Returns all moderation results collected during execution
47
- #
48
- # @return [Hash{Symbol => RubyLLM::Moderation}] Results keyed by phase
49
- def moderation_results
50
- @moderation_results || {}
51
- end
52
-
53
- private
54
-
55
- # Checks if the given phase should be moderated
56
- #
57
- # @param phase [Symbol] :input or :output
58
- # @return [Boolean] true if this phase should be moderated
59
- def should_moderate?(phase)
60
- config = resolved_moderation_config
61
- return false unless config
62
- return false if @options[:moderation] == false
63
-
64
- config[:phases].include?(phase)
65
- end
66
-
67
- # Resolves the effective moderation configuration
68
- #
69
- # Merges class-level configuration with runtime overrides.
70
- #
71
- # @return [Hash, nil] Resolved configuration or nil if disabled
72
- def resolved_moderation_config
73
- runtime_config = @options[:moderation]
74
- return nil if runtime_config == false
75
-
76
- base_config = self.class.moderation_config
77
- return nil unless base_config
78
-
79
- if runtime_config.is_a?(Hash)
80
- base_config.merge(runtime_config)
81
- else
82
- base_config
83
- end
84
- end
85
-
86
- # Performs moderation on text
87
- #
88
- # @param text [String] Text to moderate
89
- # @param phase [Symbol] :input or :output
90
- # @return [RubyLLM::Moderation] The moderation result
91
- def perform_moderation(text, phase)
92
- config = resolved_moderation_config
93
-
94
- moderation_opts = {}
95
- moderation_opts[:model] = config[:model] if config[:model]
96
-
97
- result = RubyLLM.moderate(text, **moderation_opts)
98
-
99
- @moderation_results ||= {}
100
- @moderation_results[phase] = result
101
-
102
- record_moderation_execution(result, phase)
103
-
104
- if content_flagged?(result, config, phase)
105
- handle_flagged_content(result, config, phase)
106
- end
107
-
108
- result
109
- end
110
-
111
- # Determines if content should be flagged based on result and config
112
- #
113
- # @param result [RubyLLM::Moderation] The moderation result
114
- # @param config [Hash] Moderation configuration
115
- # @param phase [Symbol] :input or :output
116
- # @return [Boolean] true if content should be flagged
117
- def content_flagged?(result, config, phase)
118
- return false unless result.flagged?
119
-
120
- # Check phase-specific or global threshold
121
- threshold = config[:"#{phase}_threshold"] || config[:threshold]
122
- if threshold
123
- max_score = result.category_scores.values.max
124
- return false if max_score.nil? || max_score < threshold
125
- end
126
-
127
- # Check category filter
128
- if config[:categories]&.any?
129
- flagged_categories = result.flagged_categories.map { |c| normalize_category(c) }
130
- allowed_categories = config[:categories].map { |c| normalize_category(c) }
131
- return false if (flagged_categories & allowed_categories).empty?
132
- end
133
-
134
- true
135
- end
136
-
137
- # Normalizes category names for comparison
138
- #
139
- # @param category [String, Symbol] Category name
140
- # @return [Symbol] Normalized category symbol
141
- def normalize_category(category)
142
- category.to_s.tr("/", "_").tr("-", "_").downcase.to_sym
143
- end
144
-
145
- # Handles flagged content according to configuration
146
- #
147
- # @param result [RubyLLM::Moderation] The moderation result
148
- # @param config [Hash] Moderation configuration
149
- # @param phase [Symbol] :input or :output
150
- # @return [void]
151
- def handle_flagged_content(result, config, phase)
152
- # Custom handler takes priority
153
- if config[:custom_handler]
154
- action = send(config[:custom_handler], result, phase)
155
- return if action == :continue
156
- end
157
-
158
- on_flagged = config[:on_flagged] || :block
159
-
160
- case on_flagged
161
- when :raise
162
- raise ModerationError.new(result, phase)
163
- when :block
164
- @moderation_blocked = true
165
- @moderation_blocked_phase = phase
166
- when :warn
167
- log_moderation_warning(result, phase)
168
- when :log
169
- log_moderation_info(result, phase)
170
- end
171
- end
172
-
173
- # Logs a moderation warning
174
- #
175
- # @param result [RubyLLM::Moderation] The moderation result
176
- # @param phase [Symbol] :input or :output
177
- # @return [void]
178
- def log_moderation_warning(result, phase)
179
- return unless defined?(Rails) && Rails.respond_to?(:logger)
180
-
181
- Rails.logger.warn(
182
- "[RubyLLM::Agents] Content flagged in #{phase} moderation: " \
183
- "#{result.flagged_categories.join(', ')}"
184
- )
185
- end
186
-
187
- # Logs moderation info
188
- #
189
- # @param result [RubyLLM::Moderation] The moderation result
190
- # @param phase [Symbol] :input or :output
191
- # @return [void]
192
- def log_moderation_info(result, phase)
193
- return unless defined?(Rails) && Rails.respond_to?(:logger)
194
-
195
- Rails.logger.info(
196
- "[RubyLLM::Agents] Content flagged in #{phase} moderation: " \
197
- "#{result.flagged_categories.join(', ')}"
198
- )
199
- end
200
-
201
- # Records moderation execution for tracking
202
- #
203
- # @param result [RubyLLM::Moderation] The moderation result
204
- # @param phase [Symbol] :input or :output
205
- # @return [void]
206
- def record_moderation_execution(result, phase)
207
- return unless RubyLLM::Agents.configuration.track_moderation
208
- return unless execution_model_available?
209
-
210
- RubyLLM::Agents::Execution.create!(
211
- agent_type: self.class.name,
212
- execution_type: "moderation",
213
- model_id: result.model,
214
- input_tokens: 0,
215
- output_tokens: 0,
216
- total_cost: 0, # Moderation is typically free or very cheap
217
- duration_ms: 0,
218
- status: result.flagged? ? "flagged" : "passed",
219
- metadata: {
220
- phase: phase,
221
- flagged: result.flagged?,
222
- flagged_categories: result.flagged_categories,
223
- category_scores: result.category_scores
224
- },
225
- tenant_id: resolved_tenant_id
226
- )
227
- rescue StandardError => e
228
- Rails.logger.warn("[RubyLLM::Agents] Failed to record moderation: #{e.message}") if defined?(Rails)
229
- end
230
-
231
- # Returns the default moderation model
232
- #
233
- # @return [String] Default moderation model identifier
234
- def default_moderation_model
235
- RubyLLM::Agents.configuration.default_moderation_model || "omni-moderation-latest"
236
- end
237
-
238
- # Builds the text to moderate for input phase
239
- #
240
- # Combines user prompt content into a single string.
241
- #
242
- # @return [String] Text to moderate
243
- def build_moderation_input
244
- prompt = user_prompt
245
- if prompt.is_a?(Array)
246
- prompt.map { |p| p.is_a?(Hash) ? p[:content] : p.to_s }.join("\n")
247
- else
248
- prompt.to_s
249
- end
250
- end
251
-
252
- # Builds a result for blocked moderation
253
- #
254
- # @param phase [Symbol] :input or :output
255
- # @return [Result] Result with moderation blocked status
256
- def build_moderation_blocked_result(phase)
257
- Result.new(
258
- content: nil,
259
- status: :"#{phase}_moderation_blocked",
260
- moderation_flagged: true,
261
- moderation_result: @moderation_results[phase],
262
- moderation_phase: phase,
263
- agent_class: self.class.name,
264
- model_id: model,
265
- input_tokens: 0,
266
- output_tokens: 0,
267
- total_cost: 0,
268
- started_at: @execution_started_at,
269
- completed_at: Time.current
270
- )
271
- end
272
- end
273
- end
274
- end
@@ -1,348 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module RubyLLM
4
- module Agents
5
- # Resolves API configuration with priority chain
6
- #
7
- # Resolution order:
8
- # 1. Tenant-specific database config (if tenant_id provided)
9
- # 2. Global database config
10
- # 3. RubyLLM.configuration (set via initializer or environment)
11
- #
12
- # This class provides a unified interface for accessing configuration
13
- # values regardless of their source, and can apply the resolved
14
- # configuration to RubyLLM.
15
- #
16
- # @example Basic resolution
17
- # resolved = ResolvedConfig.new(
18
- # tenant_config: ApiConfiguration.for_tenant("acme"),
19
- # global_config: ApiConfiguration.global,
20
- # ruby_llm_config: RubyLLM.configuration
21
- # )
22
- #
23
- # resolved.openai_api_key # => Returns from highest priority source
24
- # resolved.source_for(:openai_api_key) # => "tenant:acme"
25
- #
26
- # @example Applying to RubyLLM
27
- # resolved.apply_to_ruby_llm! # Applies all resolved values
28
- #
29
- # @see ApiConfiguration
30
- # @api public
31
- class ResolvedConfig
32
- # Returns all resolvable attributes (API keys + settings)
33
- # Lazy-loaded to avoid circular dependency with ApiConfiguration
34
- #
35
- # @return [Array<Symbol>]
36
- def self.resolvable_attributes
37
- @resolvable_attributes ||= (
38
- ApiConfiguration::API_KEY_ATTRIBUTES +
39
- ApiConfiguration::NON_KEY_ATTRIBUTES
40
- ).freeze
41
- end
42
-
43
- # @return [ApiConfiguration, nil] Tenant-specific configuration
44
- attr_reader :tenant_config
45
-
46
- # @return [ApiConfiguration, nil] Global database configuration
47
- attr_reader :global_config
48
-
49
- # @return [Object] RubyLLM configuration object
50
- attr_reader :ruby_llm_config
51
-
52
- # Creates a new resolved configuration
53
- #
54
- # @param tenant_config [ApiConfiguration, nil] Tenant-specific config
55
- # @param global_config [ApiConfiguration, nil] Global database config
56
- # @param ruby_llm_config [Object] RubyLLM.configuration
57
- def initialize(tenant_config:, global_config:, ruby_llm_config:)
58
- @tenant_config = tenant_config
59
- @global_config = global_config
60
- @ruby_llm_config = ruby_llm_config
61
- @resolved_cache = {}
62
- end
63
-
64
- # Resolves a specific attribute value using the priority chain
65
- #
66
- # @param attr_name [Symbol, String] The attribute name
67
- # @return [Object, nil] The resolved value
68
- def resolve(attr_name)
69
- attr_sym = attr_name.to_sym
70
- return @resolved_cache[attr_sym] if @resolved_cache.key?(attr_sym)
71
-
72
- @resolved_cache[attr_sym] = resolve_attribute(attr_sym)
73
- end
74
-
75
- # Returns the source of a resolved attribute value
76
- #
77
- # @param attr_name [Symbol, String] The attribute name
78
- # @return [String] Source label: "tenant:ID", "global_db", "ruby_llm_config", or "not_set"
79
- def source_for(attr_name)
80
- attr_sym = attr_name.to_sym
81
-
82
- # Check tenant config first (if present and inherits or has value)
83
- if tenant_config&.has_value?(attr_sym)
84
- return "tenant:#{tenant_config.scope_id}"
85
- end
86
-
87
- # Check global DB config (only if tenant inherits or no tenant)
88
- if should_check_global?(attr_sym) && global_config&.has_value?(attr_sym)
89
- return "global_db"
90
- end
91
-
92
- # Check RubyLLM config
93
- if ruby_llm_value_present?(attr_sym)
94
- return "ruby_llm_config"
95
- end
96
-
97
- "not_set"
98
- end
99
-
100
- # Returns all resolved values as a hash
101
- #
102
- # @return [Hash] All resolved configuration values
103
- def to_hash
104
- self.class.resolvable_attributes.each_with_object({}) do |attr, hash|
105
- value = resolve(attr)
106
- hash[attr] = value if value.present?
107
- end
108
- end
109
-
110
- # Returns a hash suitable for RubyLLM configuration
111
- #
112
- # Only includes values that differ from or override the current
113
- # RubyLLM configuration.
114
- #
115
- # @return [Hash] Configuration hash for RubyLLM
116
- def to_ruby_llm_options
117
- to_hash.slice(*ruby_llm_configurable_attributes)
118
- end
119
-
120
- # Applies the resolved configuration to RubyLLM
121
- #
122
- # This temporarily overrides RubyLLM.configuration with the
123
- # resolved values. Useful for per-request configuration.
124
- #
125
- # @return [void]
126
- def apply_to_ruby_llm!
127
- options = to_ruby_llm_options
128
- return if options.empty?
129
-
130
- RubyLLM.configure do |config|
131
- options.each do |key, value|
132
- setter = "#{key}="
133
- config.public_send(setter, value) if config.respond_to?(setter)
134
- end
135
- end
136
- end
137
-
138
- # Dynamic accessor for resolvable attributes
139
- # Uses method_missing to provide accessors without eager loading constants
140
- #
141
- # @param method_name [Symbol] The method being called
142
- # @param args [Array] Method arguments
143
- # @return [Object] The resolved value for the attribute
144
- def method_missing(method_name, *args)
145
- if self.class.resolvable_attributes.include?(method_name)
146
- resolve(method_name)
147
- else
148
- super
149
- end
150
- end
151
-
152
- # Indicates which dynamic methods are supported
153
- #
154
- # @param method_name [Symbol] The method name to check
155
- # @param include_private [Boolean] Whether to include private methods
156
- # @return [Boolean] True if the method is a resolvable attribute
157
- def respond_to_missing?(method_name, include_private = false)
158
- self.class.resolvable_attributes.include?(method_name) || super
159
- end
160
-
161
- # Returns provider status with source information
162
- #
163
- # @return [Array<Hash>] Provider status with source info
164
- def provider_statuses_with_source
165
- ApiConfiguration::PROVIDERS.map do |key, info|
166
- key_attr = info[:key_attr]
167
- value = resolve(key_attr)
168
-
169
- {
170
- key: key,
171
- name: info[:name],
172
- configured: value.present?,
173
- masked_key: value.present? ? mask_string(value) : nil,
174
- source: source_for(key_attr),
175
- capabilities: info[:capabilities]
176
- }
177
- end
178
- end
179
-
180
- # Checks if any database configuration exists
181
- #
182
- # @return [Boolean]
183
- def has_db_config?
184
- tenant_config.present? || global_config.present?
185
- end
186
-
187
- # Returns summary of configuration sources
188
- #
189
- # @return [Hash] Summary with counts per source
190
- def source_summary
191
- summary = Hash.new(0)
192
-
193
- self.class.resolvable_attributes.each do |attr|
194
- source = source_for(attr)
195
- summary[source] += 1 if resolve(attr).present?
196
- end
197
-
198
- summary
199
- end
200
-
201
- # Public method to get raw RubyLLM config value for an attribute
202
- # This returns the value from RubyLLM.configuration (initializer/ENV)
203
- # regardless of any database overrides.
204
- #
205
- # @param attr_name [Symbol, String] The attribute name
206
- # @return [Object, nil] The RubyLLM config value
207
- def ruby_llm_value_for(attr_name)
208
- ruby_llm_value(attr_name.to_sym)
209
- end
210
-
211
- # Masks a string for display (public wrapper)
212
- #
213
- # @param value [String] The string to mask
214
- # @return [String] Masked string
215
- def mask_string(value)
216
- return nil if value.blank?
217
- return "****" if value.length <= 8
218
-
219
- "#{value[0..1]}****#{value[-4..]}"
220
- end
221
-
222
- private
223
-
224
- # Resolves a single attribute using the priority chain
225
- #
226
- # @param attr_sym [Symbol] The attribute name
227
- # @return [Object, nil] The resolved value
228
- def resolve_attribute(attr_sym)
229
- # 1. Check tenant config
230
- if tenant_config&.has_value?(attr_sym)
231
- return tenant_config.send(attr_sym)
232
- end
233
-
234
- # 2. Check global DB config (if tenant allows inheritance or no tenant)
235
- if should_check_global?(attr_sym)
236
- if global_config&.has_value?(attr_sym)
237
- return global_config.send(attr_sym)
238
- end
239
- end
240
-
241
- # 3. Fall back to RubyLLM config
242
- ruby_llm_value(attr_sym)
243
- end
244
-
245
- # Determines if we should check global config
246
- #
247
- # @param attr_sym [Symbol] The attribute name
248
- # @return [Boolean]
249
- def should_check_global?(attr_sym)
250
- return true unless tenant_config
251
-
252
- # If tenant has inherit_global_defaults enabled, check global
253
- tenant_config.inherit_global_defaults != false
254
- end
255
-
256
- # Gets a value from RubyLLM configuration
257
- #
258
- # @param attr_sym [Symbol] The attribute name
259
- # @return [Object, nil]
260
- def ruby_llm_value(attr_sym)
261
- return nil unless ruby_llm_config
262
-
263
- # Map our attribute names to RubyLLM config method names
264
- method_name = ruby_llm_config_mapping(attr_sym)
265
- return nil unless method_name
266
-
267
- if ruby_llm_config.respond_to?(method_name)
268
- ruby_llm_config.send(method_name)
269
- end
270
- rescue StandardError
271
- nil
272
- end
273
-
274
- # Checks if a RubyLLM config value is present
275
- #
276
- # @param attr_sym [Symbol] The attribute name
277
- # @return [Boolean]
278
- def ruby_llm_value_present?(attr_sym)
279
- value = ruby_llm_value(attr_sym)
280
- value.present?
281
- end
282
-
283
- # Maps our attribute names to RubyLLM configuration method names
284
- #
285
- # @param attr_sym [Symbol] Our attribute name
286
- # @return [Symbol, nil] RubyLLM config method name
287
- def ruby_llm_config_mapping(attr_sym)
288
- # Most attributes map directly
289
- mapping = {
290
- openai_api_key: :openai_api_key,
291
- anthropic_api_key: :anthropic_api_key,
292
- gemini_api_key: :gemini_api_key,
293
- deepseek_api_key: :deepseek_api_key,
294
- mistral_api_key: :mistral_api_key,
295
- perplexity_api_key: :perplexity_api_key,
296
- openrouter_api_key: :openrouter_api_key,
297
- gpustack_api_key: :gpustack_api_key,
298
- xai_api_key: :xai_api_key,
299
- ollama_api_key: :ollama_api_key,
300
- bedrock_api_key: :bedrock_api_key,
301
- bedrock_secret_key: :bedrock_secret_key,
302
- bedrock_session_token: :bedrock_session_token,
303
- bedrock_region: :bedrock_region,
304
- vertexai_credentials: :vertexai_credentials,
305
- vertexai_project_id: :vertexai_project_id,
306
- vertexai_location: :vertexai_location,
307
- openai_api_base: :openai_api_base,
308
- gemini_api_base: :gemini_api_base,
309
- ollama_api_base: :ollama_api_base,
310
- gpustack_api_base: :gpustack_api_base,
311
- xai_api_base: :xai_api_base,
312
- openai_organization_id: :openai_organization_id,
313
- openai_project_id: :openai_project_id,
314
- default_model: :default_model,
315
- default_embedding_model: :default_embedding_model,
316
- default_image_model: :default_image_model,
317
- default_moderation_model: :default_moderation_model,
318
- request_timeout: :request_timeout,
319
- max_retries: :max_retries,
320
- retry_interval: :retry_interval,
321
- retry_backoff_factor: :retry_backoff_factor,
322
- retry_interval_randomness: :retry_interval_randomness,
323
- http_proxy: :http_proxy
324
- }
325
-
326
- mapping[attr_sym]
327
- end
328
-
329
- # Returns attributes that can be set on RubyLLM configuration
330
- #
331
- # @return [Array<Symbol>]
332
- def ruby_llm_configurable_attributes
333
- ApiConfiguration::API_KEY_ATTRIBUTES +
334
- ApiConfiguration::ENDPOINT_ATTRIBUTES +
335
- ApiConfiguration::MODEL_ATTRIBUTES +
336
- ApiConfiguration::CONNECTION_ATTRIBUTES +
337
- %i[
338
- openai_organization_id
339
- openai_project_id
340
- bedrock_region
341
- vertexai_project_id
342
- vertexai_location
343
- ]
344
- end
345
-
346
- end
347
- end
348
- end