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.
- checksums.yaml +4 -4
- data/README.md +101 -334
- data/app/controllers/concerns/ruby_llm/agents/sortable.rb +0 -1
- data/app/controllers/ruby_llm/agents/agents_controller.rb +5 -56
- data/app/controllers/ruby_llm/agents/dashboard_controller.rb +22 -106
- data/app/controllers/ruby_llm/agents/executions_controller.rb +4 -114
- data/app/controllers/ruby_llm/agents/tenants_controller.rb +30 -2
- data/app/helpers/ruby_llm/agents/application_helper.rb +19 -53
- data/app/models/ruby_llm/agents/execution/analytics.rb +13 -54
- data/app/models/ruby_llm/agents/execution/scopes.rb +61 -14
- data/app/models/ruby_llm/agents/execution.rb +46 -10
- data/app/models/ruby_llm/agents/execution_detail.rb +18 -0
- data/app/models/ruby_llm/agents/tenant/budgetable.rb +132 -24
- data/app/models/ruby_llm/agents/tenant/incrementable.rb +117 -0
- data/app/models/ruby_llm/agents/tenant/resettable.rb +128 -0
- data/app/models/ruby_llm/agents/tenant/trackable.rb +46 -12
- data/app/models/ruby_llm/agents/tenant.rb +2 -3
- data/app/models/ruby_llm/agents/tenant_budget.rb +6 -3
- data/app/services/ruby_llm/agents/agent_registry.rb +6 -112
- data/app/views/layouts/ruby_llm/agents/application.html.erb +87 -252
- data/app/views/ruby_llm/agents/agents/_config_agent.html.erb +71 -218
- data/app/views/ruby_llm/agents/agents/_config_embedder.html.erb +20 -63
- data/app/views/ruby_llm/agents/agents/_config_image_generator.html.erb +44 -131
- data/app/views/ruby_llm/agents/agents/_config_moderator.html.erb +16 -57
- data/app/views/ruby_llm/agents/agents/_config_speaker.html.erb +39 -104
- data/app/views/ruby_llm/agents/agents/_config_transcriber.html.erb +29 -82
- data/app/views/ruby_llm/agents/agents/_empty_state.html.erb +4 -14
- data/app/views/ruby_llm/agents/agents/index.html.erb +105 -274
- data/app/views/ruby_llm/agents/agents/show.html.erb +248 -378
- data/app/views/ruby_llm/agents/dashboard/_action_center.html.erb +29 -52
- data/app/views/ruby_llm/agents/dashboard/_tenant_budget.html.erb +73 -99
- data/app/views/ruby_llm/agents/dashboard/index.html.erb +228 -433
- data/app/views/ruby_llm/agents/executions/_execution.html.erb +1 -1
- data/app/views/ruby_llm/agents/executions/_filters.html.erb +4 -25
- data/app/views/ruby_llm/agents/executions/_list.html.erb +111 -152
- data/app/views/ruby_llm/agents/executions/index.html.erb +5 -7
- data/app/views/ruby_llm/agents/executions/show.html.erb +528 -989
- data/app/views/ruby_llm/agents/shared/_agent_type_badge.html.erb +5 -21
- data/app/views/ruby_llm/agents/shared/_executions_table.html.erb +70 -191
- data/app/views/ruby_llm/agents/shared/_filter_dropdown.html.erb +16 -44
- data/app/views/ruby_llm/agents/shared/_select_dropdown.html.erb +12 -41
- data/app/views/ruby_llm/agents/shared/_status_badge.html.erb +11 -65
- data/app/views/ruby_llm/agents/shared/_tenant_filter.html.erb +6 -5
- data/app/views/ruby_llm/agents/system_config/show.html.erb +240 -351
- data/app/views/ruby_llm/agents/tenants/_form.html.erb +67 -77
- data/app/views/ruby_llm/agents/tenants/edit.html.erb +7 -9
- data/app/views/ruby_llm/agents/tenants/index.html.erb +100 -122
- data/app/views/ruby_llm/agents/tenants/show.html.erb +146 -336
- data/config/routes.rb +0 -13
- data/lib/generators/ruby_llm_agents/install_generator.rb +9 -14
- data/lib/generators/ruby_llm_agents/migrate_structure_generator.rb +2 -12
- data/lib/generators/ruby_llm_agents/restructure_generator.rb +0 -2
- data/lib/generators/ruby_llm_agents/templates/add_usage_counters_to_tenants_migration.rb.tt +37 -0
- data/lib/generators/ruby_llm_agents/templates/agent.rb.tt +1 -2
- data/lib/generators/ruby_llm_agents/templates/application_agent.rb.tt +1 -1
- data/lib/generators/ruby_llm_agents/templates/application_image_pipeline.rb.tt +0 -1
- data/lib/generators/ruby_llm_agents/templates/create_execution_details_migration.rb.tt +27 -0
- data/lib/generators/ruby_llm_agents/templates/create_tenants_migration.rb.tt +25 -0
- data/lib/generators/ruby_llm_agents/templates/image_pipeline.rb.tt +0 -1
- data/lib/generators/ruby_llm_agents/templates/initializer.rb.tt +9 -12
- data/lib/generators/ruby_llm_agents/templates/migration.rb.tt +40 -71
- data/lib/generators/ruby_llm_agents/templates/remove_agent_version_migration.rb.tt +13 -0
- data/lib/generators/ruby_llm_agents/templates/remove_workflow_columns_migration.rb.tt +19 -0
- data/lib/generators/ruby_llm_agents/templates/skills/AGENTS.md.tt +2 -4
- data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_PIPELINES.md.tt +0 -1
- data/lib/generators/ruby_llm_agents/templates/split_execution_details_migration.rb.tt +232 -0
- data/lib/generators/ruby_llm_agents/upgrade_generator.rb +58 -262
- data/lib/ruby_llm/agents/audio/speaker.rb +0 -1
- data/lib/ruby_llm/agents/audio/transcriber.rb +0 -1
- data/lib/ruby_llm/agents/base_agent.rb +52 -6
- data/lib/ruby_llm/agents/core/base/callbacks.rb +142 -0
- data/lib/ruby_llm/agents/core/base.rb +23 -55
- data/lib/ruby_llm/agents/core/configuration.rb +58 -117
- data/lib/ruby_llm/agents/core/errors.rb +0 -58
- data/lib/ruby_llm/agents/core/instrumentation.rb +157 -110
- data/lib/ruby_llm/agents/core/llm_tenant.rb +8 -7
- data/lib/ruby_llm/agents/core/version.rb +1 -1
- data/lib/ruby_llm/agents/dsl/base.rb +157 -17
- data/lib/ruby_llm/agents/dsl/caching.rb +33 -2
- data/lib/ruby_llm/agents/dsl/reliability.rb +148 -0
- data/lib/ruby_llm/agents/dsl.rb +1 -2
- data/lib/ruby_llm/agents/image/analyzer/execution.rb +1 -2
- data/lib/ruby_llm/agents/image/background_remover/execution.rb +1 -2
- data/lib/ruby_llm/agents/image/concerns/image_operation_dsl.rb +1 -13
- data/lib/ruby_llm/agents/image/concerns/image_operation_execution.rb +2 -2
- data/lib/ruby_llm/agents/image/editor/dsl.rb +0 -14
- data/lib/ruby_llm/agents/image/editor/execution.rb +1 -10
- data/lib/ruby_llm/agents/image/editor.rb +0 -1
- data/lib/ruby_llm/agents/image/generator.rb +0 -21
- data/lib/ruby_llm/agents/image/pipeline/dsl.rb +0 -13
- data/lib/ruby_llm/agents/image/pipeline/execution.rb +0 -1
- data/lib/ruby_llm/agents/image/transformer/dsl.rb +0 -13
- data/lib/ruby_llm/agents/image/transformer/execution.rb +1 -10
- data/lib/ruby_llm/agents/image/transformer.rb +0 -1
- data/lib/ruby_llm/agents/image/upscaler/execution.rb +1 -2
- data/lib/ruby_llm/agents/image/variator/execution.rb +1 -2
- data/lib/ruby_llm/agents/infrastructure/alert_manager.rb +78 -173
- data/lib/ruby_llm/agents/infrastructure/attempt_tracker.rb +1 -0
- data/lib/ruby_llm/agents/infrastructure/budget/budget_query.rb +66 -2
- data/lib/ruby_llm/agents/infrastructure/budget/spend_recorder.rb +0 -12
- data/lib/ruby_llm/agents/infrastructure/circuit_breaker.rb +10 -13
- data/lib/ruby_llm/agents/infrastructure/reliability.rb +37 -2
- data/lib/ruby_llm/agents/pipeline/context.rb +0 -1
- data/lib/ruby_llm/agents/pipeline/middleware/budget.rb +28 -4
- data/lib/ruby_llm/agents/pipeline/middleware/cache.rb +3 -10
- data/lib/ruby_llm/agents/pipeline/middleware/instrumentation.rb +88 -55
- data/lib/ruby_llm/agents/pipeline/middleware/tenant.rb +5 -41
- data/lib/ruby_llm/agents/rails/engine.rb +6 -6
- data/lib/ruby_llm/agents/results/base.rb +1 -49
- data/lib/ruby_llm/agents/text/embedder.rb +0 -1
- data/lib/ruby_llm/agents.rb +1 -9
- data/lib/tasks/ruby_llm_agents.rake +34 -0
- metadata +12 -81
- data/app/controllers/ruby_llm/agents/api_configurations_controller.rb +0 -214
- data/app/controllers/ruby_llm/agents/workflows_controller.rb +0 -544
- data/app/mailers/ruby_llm/agents/alert_mailer.rb +0 -84
- data/app/mailers/ruby_llm/agents/application_mailer.rb +0 -28
- data/app/models/ruby_llm/agents/api_configuration.rb +0 -386
- data/app/models/ruby_llm/agents/execution/workflow.rb +0 -170
- data/app/models/ruby_llm/agents/tenant/configurable.rb +0 -135
- data/app/views/ruby_llm/agents/agents/_agent.html.erb +0 -98
- data/app/views/ruby_llm/agents/agents/_version_comparison.html.erb +0 -186
- data/app/views/ruby_llm/agents/agents/_workflow.html.erb +0 -126
- data/app/views/ruby_llm/agents/alert_mailer/alert_notification.html.erb +0 -107
- data/app/views/ruby_llm/agents/alert_mailer/alert_notification.text.erb +0 -18
- data/app/views/ruby_llm/agents/api_configurations/_api_key_field.html.erb +0 -34
- data/app/views/ruby_llm/agents/api_configurations/_form.html.erb +0 -288
- data/app/views/ruby_llm/agents/api_configurations/edit.html.erb +0 -95
- data/app/views/ruby_llm/agents/api_configurations/edit_tenant.html.erb +0 -97
- data/app/views/ruby_llm/agents/api_configurations/show.html.erb +0 -214
- data/app/views/ruby_llm/agents/api_configurations/tenant.html.erb +0 -179
- data/app/views/ruby_llm/agents/dashboard/_agent_comparison.html.erb +0 -73
- data/app/views/ruby_llm/agents/dashboard/_alerts_feed.html.erb +0 -62
- data/app/views/ruby_llm/agents/dashboard/_breaker_strip.html.erb +0 -47
- data/app/views/ruby_llm/agents/dashboard/_budgets_bar.html.erb +0 -75
- data/app/views/ruby_llm/agents/dashboard/_model_comparison.html.erb +0 -56
- data/app/views/ruby_llm/agents/dashboard/_model_cost_breakdown.html.erb +0 -115
- data/app/views/ruby_llm/agents/dashboard/_now_strip.html.erb +0 -59
- data/app/views/ruby_llm/agents/dashboard/_top_errors.html.erb +0 -60
- data/app/views/ruby_llm/agents/executions/_workflow_summary.html.erb +0 -86
- data/app/views/ruby_llm/agents/executions/dry_run.html.erb +0 -149
- data/app/views/ruby_llm/agents/shared/_breadcrumbs.html.erb +0 -48
- data/app/views/ruby_llm/agents/shared/_nav_link.html.erb +0 -27
- data/app/views/ruby_llm/agents/shared/_stat_card.html.erb +0 -14
- data/app/views/ruby_llm/agents/shared/_workflow_type_badge.html.erb +0 -35
- data/app/views/ruby_llm/agents/workflows/_empty_state.html.erb +0 -22
- data/app/views/ruby_llm/agents/workflows/_step_performance.html.erb +0 -228
- data/app/views/ruby_llm/agents/workflows/_structure_dsl.html.erb +0 -539
- data/app/views/ruby_llm/agents/workflows/_structure_parallel.html.erb +0 -76
- data/app/views/ruby_llm/agents/workflows/_structure_pipeline.html.erb +0 -74
- data/app/views/ruby_llm/agents/workflows/_structure_router.html.erb +0 -108
- data/app/views/ruby_llm/agents/workflows/_workflow_diagram.html.erb +0 -920
- data/app/views/ruby_llm/agents/workflows/index.html.erb +0 -179
- data/app/views/ruby_llm/agents/workflows/show.html.erb +0 -467
- data/lib/generators/ruby_llm_agents/api_configuration_generator.rb +0 -100
- data/lib/generators/ruby_llm_agents/templates/add_workflow_migration.rb.tt +0 -38
- data/lib/generators/ruby_llm_agents/templates/application_workflow.rb.tt +0 -48
- data/lib/generators/ruby_llm_agents/templates/create_api_configurations_migration.rb.tt +0 -90
- data/lib/generators/ruby_llm_agents/templates/skills/WORKFLOWS.md.tt +0 -551
- data/lib/ruby_llm/agents/core/base/moderation_dsl.rb +0 -181
- data/lib/ruby_llm/agents/core/base/moderation_execution.rb +0 -274
- data/lib/ruby_llm/agents/core/resolved_config.rb +0 -348
- data/lib/ruby_llm/agents/image/generator/content_policy.rb +0 -95
- data/lib/ruby_llm/agents/infrastructure/redactor.rb +0 -130
- data/lib/ruby_llm/agents/results/moderation_result.rb +0 -158
- data/lib/ruby_llm/agents/text/moderator.rb +0 -237
- data/lib/ruby_llm/agents/workflow/approval.rb +0 -205
- data/lib/ruby_llm/agents/workflow/approval_store.rb +0 -179
- data/lib/ruby_llm/agents/workflow/async.rb +0 -220
- data/lib/ruby_llm/agents/workflow/async_executor.rb +0 -156
- data/lib/ruby_llm/agents/workflow/dsl/executor.rb +0 -467
- data/lib/ruby_llm/agents/workflow/dsl/input_schema.rb +0 -244
- data/lib/ruby_llm/agents/workflow/dsl/iteration_executor.rb +0 -289
- data/lib/ruby_llm/agents/workflow/dsl/parallel_group.rb +0 -107
- data/lib/ruby_llm/agents/workflow/dsl/route_builder.rb +0 -150
- data/lib/ruby_llm/agents/workflow/dsl/schedule_helpers.rb +0 -187
- data/lib/ruby_llm/agents/workflow/dsl/step_config.rb +0 -352
- data/lib/ruby_llm/agents/workflow/dsl/step_executor.rb +0 -415
- data/lib/ruby_llm/agents/workflow/dsl/wait_config.rb +0 -257
- data/lib/ruby_llm/agents/workflow/dsl/wait_executor.rb +0 -317
- data/lib/ruby_llm/agents/workflow/dsl.rb +0 -576
- data/lib/ruby_llm/agents/workflow/instrumentation.rb +0 -249
- data/lib/ruby_llm/agents/workflow/notifiers/base.rb +0 -117
- data/lib/ruby_llm/agents/workflow/notifiers/email.rb +0 -117
- data/lib/ruby_llm/agents/workflow/notifiers/slack.rb +0 -180
- data/lib/ruby_llm/agents/workflow/notifiers/webhook.rb +0 -121
- data/lib/ruby_llm/agents/workflow/notifiers.rb +0 -70
- data/lib/ruby_llm/agents/workflow/orchestrator.rb +0 -416
- data/lib/ruby_llm/agents/workflow/result.rb +0 -592
- data/lib/ruby_llm/agents/workflow/thread_pool.rb +0 -185
- data/lib/ruby_llm/agents/workflow/throttle_manager.rb +0 -206
- 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
|