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,95 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module RubyLLM
|
|
4
|
-
module Agents
|
|
5
|
-
class ImageGenerator
|
|
6
|
-
# Content policy enforcement for image generation prompts
|
|
7
|
-
#
|
|
8
|
-
# Validates prompts against configurable policy levels to prevent
|
|
9
|
-
# generation of inappropriate content.
|
|
10
|
-
#
|
|
11
|
-
# @example Using content policy in a generator
|
|
12
|
-
# class SafeImageGenerator < RubyLLM::Agents::ImageGenerator
|
|
13
|
-
# content_policy :strict
|
|
14
|
-
# end
|
|
15
|
-
#
|
|
16
|
-
# @example Manual validation
|
|
17
|
-
# ContentPolicy.validate!("A beautiful sunset", :moderate)
|
|
18
|
-
# # => nil (passes)
|
|
19
|
-
#
|
|
20
|
-
# ContentPolicy.validate!("Violent scene", :strict)
|
|
21
|
-
# # => raises ContentPolicyViolation
|
|
22
|
-
#
|
|
23
|
-
module ContentPolicy
|
|
24
|
-
# Blocked patterns by policy level
|
|
25
|
-
#
|
|
26
|
-
# :strict - Blocks violence, nudity, hate, weapons, drugs
|
|
27
|
-
# :moderate - Blocks explicit content, gore, hate speech
|
|
28
|
-
# :standard - No blocking (relies on model's built-in filters)
|
|
29
|
-
# :none - No validation at all
|
|
30
|
-
#
|
|
31
|
-
BLOCKED_PATTERNS = {
|
|
32
|
-
strict: [
|
|
33
|
-
/\b(violence|violent|gore|blood|death|kill|murder)\b/i,
|
|
34
|
-
/\b(nude|naked|nsfw|explicit|sexual|porn)\b/i,
|
|
35
|
-
/\b(hate|racist|discrimination|slur)\b/i,
|
|
36
|
-
/\b(weapon|gun|knife|bomb|explosive)\b/i,
|
|
37
|
-
/\b(drug|cocaine|heroin|meth)\b/i
|
|
38
|
-
],
|
|
39
|
-
moderate: [
|
|
40
|
-
/\b(nude|naked|nsfw|explicit|sexual|porn)\b/i,
|
|
41
|
-
/\b(gore|graphic.?violence)\b/i,
|
|
42
|
-
/\b(hate.?speech|slur)\b/i
|
|
43
|
-
],
|
|
44
|
-
standard: [],
|
|
45
|
-
none: []
|
|
46
|
-
}.freeze
|
|
47
|
-
|
|
48
|
-
class << self
|
|
49
|
-
# Validate a prompt against a policy level
|
|
50
|
-
#
|
|
51
|
-
# @param prompt [String] The prompt to validate
|
|
52
|
-
# @param level [Symbol] Policy level (:none, :standard, :moderate, :strict)
|
|
53
|
-
# @raise [ContentPolicyViolation] If prompt violates the policy
|
|
54
|
-
def validate!(prompt, level)
|
|
55
|
-
return if level == :none || level.nil?
|
|
56
|
-
|
|
57
|
-
patterns = BLOCKED_PATTERNS[level.to_sym] || BLOCKED_PATTERNS[:standard]
|
|
58
|
-
|
|
59
|
-
patterns.each do |pattern|
|
|
60
|
-
if prompt.match?(pattern)
|
|
61
|
-
raise ContentPolicyViolation,
|
|
62
|
-
"Prompt contains content blocked by #{level} policy"
|
|
63
|
-
end
|
|
64
|
-
end
|
|
65
|
-
end
|
|
66
|
-
|
|
67
|
-
# Check if a prompt passes the policy (non-raising version)
|
|
68
|
-
#
|
|
69
|
-
# @param prompt [String] The prompt to check
|
|
70
|
-
# @param level [Symbol] Policy level
|
|
71
|
-
# @return [Boolean] true if prompt passes
|
|
72
|
-
def valid?(prompt, level)
|
|
73
|
-
validate!(prompt, level)
|
|
74
|
-
true
|
|
75
|
-
rescue ContentPolicyViolation
|
|
76
|
-
false
|
|
77
|
-
end
|
|
78
|
-
|
|
79
|
-
# Get the matched pattern for a violation (for debugging)
|
|
80
|
-
#
|
|
81
|
-
# @param prompt [String] The prompt to check
|
|
82
|
-
# @param level [Symbol] Policy level
|
|
83
|
-
# @return [Regexp, nil] The matched pattern or nil
|
|
84
|
-
def matched_pattern(prompt, level)
|
|
85
|
-
patterns = BLOCKED_PATTERNS[level.to_sym] || []
|
|
86
|
-
patterns.find { |pattern| prompt.match?(pattern) }
|
|
87
|
-
end
|
|
88
|
-
end
|
|
89
|
-
end
|
|
90
|
-
|
|
91
|
-
# Exception raised when a prompt violates content policy
|
|
92
|
-
class ContentPolicyViolation < StandardError; end
|
|
93
|
-
end
|
|
94
|
-
end
|
|
95
|
-
end
|
|
@@ -1,130 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module RubyLLM
|
|
4
|
-
module Agents
|
|
5
|
-
# Unified redaction utility for PII and sensitive data
|
|
6
|
-
#
|
|
7
|
-
# Provides methods to redact sensitive information from hashes, arrays, and strings
|
|
8
|
-
# based on configurable field names and regex patterns.
|
|
9
|
-
#
|
|
10
|
-
# @example Redacting a hash
|
|
11
|
-
# Redactor.redact({ password: "secret", name: "John" })
|
|
12
|
-
# # => { password: "[REDACTED]", name: "John" }
|
|
13
|
-
#
|
|
14
|
-
# @example Redacting a string with patterns
|
|
15
|
-
# Redactor.redact_string("SSN: 123-45-6789")
|
|
16
|
-
# # => "SSN: [REDACTED]"
|
|
17
|
-
#
|
|
18
|
-
# @see RubyLLM::Agents::Configuration
|
|
19
|
-
# @api public
|
|
20
|
-
module Redactor
|
|
21
|
-
class << self
|
|
22
|
-
# Redacts sensitive data from an object (hash, array, or primitive)
|
|
23
|
-
#
|
|
24
|
-
# @param obj [Object] The object to redact
|
|
25
|
-
# @param config [Configuration, nil] Optional configuration override
|
|
26
|
-
# @return [Object] The redacted object (new object, original not modified)
|
|
27
|
-
def redact(obj, config = nil)
|
|
28
|
-
config ||= RubyLLM::Agents.configuration
|
|
29
|
-
|
|
30
|
-
case obj
|
|
31
|
-
when Hash
|
|
32
|
-
redact_hash(obj, config)
|
|
33
|
-
when Array
|
|
34
|
-
redact_array(obj, config)
|
|
35
|
-
when String
|
|
36
|
-
redact_string(obj, config)
|
|
37
|
-
else
|
|
38
|
-
obj
|
|
39
|
-
end
|
|
40
|
-
end
|
|
41
|
-
|
|
42
|
-
# Redacts sensitive data from a string using configured patterns
|
|
43
|
-
#
|
|
44
|
-
# @param str [String, nil] The string to redact
|
|
45
|
-
# @param config [Configuration, nil] Optional configuration override
|
|
46
|
-
# @return [String, nil] The redacted string
|
|
47
|
-
def redact_string(str, config = nil)
|
|
48
|
-
return nil if str.nil?
|
|
49
|
-
return str unless str.is_a?(String)
|
|
50
|
-
|
|
51
|
-
config ||= RubyLLM::Agents.configuration
|
|
52
|
-
result = str.dup
|
|
53
|
-
|
|
54
|
-
# Apply pattern-based redaction
|
|
55
|
-
config.redaction_patterns.each do |pattern|
|
|
56
|
-
result = result.gsub(pattern, config.redaction_placeholder)
|
|
57
|
-
end
|
|
58
|
-
|
|
59
|
-
# Truncate if max length is configured
|
|
60
|
-
max_length = config.redaction_max_value_length
|
|
61
|
-
if max_length && result.length > max_length
|
|
62
|
-
result = result[0, max_length] + "..."
|
|
63
|
-
end
|
|
64
|
-
|
|
65
|
-
result
|
|
66
|
-
end
|
|
67
|
-
|
|
68
|
-
private
|
|
69
|
-
|
|
70
|
-
# Redacts sensitive fields from a hash
|
|
71
|
-
#
|
|
72
|
-
# @param hash [Hash] The hash to redact
|
|
73
|
-
# @param config [Configuration] The configuration
|
|
74
|
-
# @return [Hash] The redacted hash
|
|
75
|
-
def redact_hash(hash, config)
|
|
76
|
-
hash.each_with_object({}) do |(key, value), result|
|
|
77
|
-
key_str = key.to_s.downcase
|
|
78
|
-
|
|
79
|
-
if sensitive_field?(key_str, config)
|
|
80
|
-
result[key] = config.redaction_placeholder
|
|
81
|
-
else
|
|
82
|
-
result[key] = redact_value(value, config)
|
|
83
|
-
end
|
|
84
|
-
end
|
|
85
|
-
end
|
|
86
|
-
|
|
87
|
-
# Redacts sensitive values from an array
|
|
88
|
-
#
|
|
89
|
-
# @param array [Array] The array to redact
|
|
90
|
-
# @param config [Configuration] The configuration
|
|
91
|
-
# @return [Array] The redacted array
|
|
92
|
-
def redact_array(array, config)
|
|
93
|
-
array.map { |item| redact(item, config) }
|
|
94
|
-
end
|
|
95
|
-
|
|
96
|
-
# Redacts a single value based on its type
|
|
97
|
-
#
|
|
98
|
-
# @param value [Object] The value to redact
|
|
99
|
-
# @param config [Configuration] The configuration
|
|
100
|
-
# @return [Object] The redacted value
|
|
101
|
-
def redact_value(value, config)
|
|
102
|
-
case value
|
|
103
|
-
when Hash
|
|
104
|
-
redact_hash(value, config)
|
|
105
|
-
when Array
|
|
106
|
-
redact_array(value, config)
|
|
107
|
-
when String
|
|
108
|
-
redact_string(value, config)
|
|
109
|
-
when defined?(ActiveRecord::Base) && ActiveRecord::Base
|
|
110
|
-
# Convert ActiveRecord objects to safe references
|
|
111
|
-
{ id: value&.id, type: value&.class&.name }
|
|
112
|
-
else
|
|
113
|
-
value
|
|
114
|
-
end
|
|
115
|
-
end
|
|
116
|
-
|
|
117
|
-
# Checks if a field name is sensitive
|
|
118
|
-
#
|
|
119
|
-
# @param field_name [String] The field name to check (lowercase)
|
|
120
|
-
# @param config [Configuration] The configuration
|
|
121
|
-
# @return [Boolean] true if the field should be redacted
|
|
122
|
-
def sensitive_field?(field_name, config)
|
|
123
|
-
config.redaction_fields.any? do |sensitive|
|
|
124
|
-
field_name.include?(sensitive.downcase)
|
|
125
|
-
end
|
|
126
|
-
end
|
|
127
|
-
end
|
|
128
|
-
end
|
|
129
|
-
end
|
|
130
|
-
end
|
|
@@ -1,158 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module RubyLLM
|
|
4
|
-
module Agents
|
|
5
|
-
# Wrapper for moderation results with threshold and category filtering
|
|
6
|
-
#
|
|
7
|
-
# Provides a filtered view of moderation results based on configured
|
|
8
|
-
# thresholds and category filters. The raw result is still accessible
|
|
9
|
-
# for full details.
|
|
10
|
-
#
|
|
11
|
-
# @example Basic usage
|
|
12
|
-
# result = ModerationResult.new(
|
|
13
|
-
# result: raw_moderation,
|
|
14
|
-
# threshold: 0.8,
|
|
15
|
-
# categories: [:hate, :violence]
|
|
16
|
-
# )
|
|
17
|
-
#
|
|
18
|
-
# result.flagged? # Only true if score >= 0.8 AND category is hate or violence
|
|
19
|
-
# result.passed? # Opposite of flagged?
|
|
20
|
-
#
|
|
21
|
-
# @api public
|
|
22
|
-
class ModerationResult
|
|
23
|
-
# @return [Object] The raw moderation result from RubyLLM
|
|
24
|
-
attr_reader :raw_result
|
|
25
|
-
|
|
26
|
-
# @return [Float, nil] Configured threshold for flagging
|
|
27
|
-
attr_reader :threshold
|
|
28
|
-
|
|
29
|
-
# @return [Array<Symbol>, nil] Categories to filter on
|
|
30
|
-
attr_reader :filter_categories
|
|
31
|
-
|
|
32
|
-
# Creates a new ModerationResult
|
|
33
|
-
#
|
|
34
|
-
# @param result [Object] Raw moderation result from RubyLLM
|
|
35
|
-
# @param threshold [Float, nil] Score threshold (0.0-1.0)
|
|
36
|
-
# @param categories [Array<Symbol>, nil] Categories to check
|
|
37
|
-
def initialize(result:, threshold: nil, categories: nil)
|
|
38
|
-
@raw_result = result
|
|
39
|
-
@threshold = threshold
|
|
40
|
-
@filter_categories = categories&.map(&:to_sym)
|
|
41
|
-
end
|
|
42
|
-
|
|
43
|
-
# Returns whether the content should be flagged
|
|
44
|
-
#
|
|
45
|
-
# Considers both threshold and category filters if configured.
|
|
46
|
-
# Content is flagged only if:
|
|
47
|
-
# - Raw result is flagged AND
|
|
48
|
-
# - Score meets threshold (if configured) AND
|
|
49
|
-
# - Category matches filter (if configured)
|
|
50
|
-
#
|
|
51
|
-
# @return [Boolean] true if content should be flagged
|
|
52
|
-
def flagged?
|
|
53
|
-
return false unless raw_result.flagged?
|
|
54
|
-
|
|
55
|
-
passes_threshold? && passes_category_filter?
|
|
56
|
-
end
|
|
57
|
-
|
|
58
|
-
# Returns whether the content passed moderation
|
|
59
|
-
#
|
|
60
|
-
# @return [Boolean] true if content is not flagged
|
|
61
|
-
def passed?
|
|
62
|
-
!flagged?
|
|
63
|
-
end
|
|
64
|
-
|
|
65
|
-
# Returns the flagged categories, filtered by configuration
|
|
66
|
-
#
|
|
67
|
-
# @return [Array<String, Symbol>] Categories that triggered flagging
|
|
68
|
-
def flagged_categories
|
|
69
|
-
cats = raw_result.flagged_categories || []
|
|
70
|
-
return cats unless filter_categories&.any?
|
|
71
|
-
|
|
72
|
-
cats.select { |c| filter_categories.include?(normalize_category(c)) }
|
|
73
|
-
end
|
|
74
|
-
|
|
75
|
-
# Returns all category scores from the raw result
|
|
76
|
-
#
|
|
77
|
-
# @return [Hash{String, Symbol => Float}] Category to score mapping
|
|
78
|
-
def category_scores
|
|
79
|
-
raw_result.category_scores || {}
|
|
80
|
-
end
|
|
81
|
-
|
|
82
|
-
# Returns the moderation result ID
|
|
83
|
-
#
|
|
84
|
-
# @return [String, nil] Result identifier
|
|
85
|
-
def id
|
|
86
|
-
raw_result.id
|
|
87
|
-
end
|
|
88
|
-
|
|
89
|
-
# Returns the model used for moderation
|
|
90
|
-
#
|
|
91
|
-
# @return [String, nil] Model identifier
|
|
92
|
-
def model
|
|
93
|
-
raw_result.model
|
|
94
|
-
end
|
|
95
|
-
|
|
96
|
-
# Returns the maximum score across all categories
|
|
97
|
-
#
|
|
98
|
-
# @return [Float] Highest category score
|
|
99
|
-
def max_score
|
|
100
|
-
scores = category_scores.values
|
|
101
|
-
scores.any? ? scores.max : 0.0
|
|
102
|
-
end
|
|
103
|
-
|
|
104
|
-
# Returns whether the raw result was flagged (ignoring filters)
|
|
105
|
-
#
|
|
106
|
-
# @return [Boolean] true if raw result was flagged
|
|
107
|
-
def raw_flagged?
|
|
108
|
-
raw_result.flagged?
|
|
109
|
-
end
|
|
110
|
-
|
|
111
|
-
# Converts the result to a hash
|
|
112
|
-
#
|
|
113
|
-
# @return [Hash] Result data as a hash
|
|
114
|
-
def to_h
|
|
115
|
-
{
|
|
116
|
-
flagged: flagged?,
|
|
117
|
-
raw_flagged: raw_flagged?,
|
|
118
|
-
flagged_categories: flagged_categories,
|
|
119
|
-
category_scores: category_scores,
|
|
120
|
-
max_score: max_score,
|
|
121
|
-
threshold: threshold,
|
|
122
|
-
filter_categories: filter_categories,
|
|
123
|
-
model: model,
|
|
124
|
-
id: id
|
|
125
|
-
}
|
|
126
|
-
end
|
|
127
|
-
|
|
128
|
-
private
|
|
129
|
-
|
|
130
|
-
# Checks if the max score meets the threshold
|
|
131
|
-
#
|
|
132
|
-
# @return [Boolean] true if threshold is met or not configured
|
|
133
|
-
def passes_threshold?
|
|
134
|
-
return true unless threshold
|
|
135
|
-
|
|
136
|
-
max_score >= threshold
|
|
137
|
-
end
|
|
138
|
-
|
|
139
|
-
# Checks if any flagged categories match the filter
|
|
140
|
-
#
|
|
141
|
-
# @return [Boolean] true if categories match or no filter configured
|
|
142
|
-
def passes_category_filter?
|
|
143
|
-
return true unless filter_categories&.any?
|
|
144
|
-
|
|
145
|
-
normalized_flagged = (raw_result.flagged_categories || []).map { |c| normalize_category(c) }
|
|
146
|
-
(normalized_flagged & filter_categories).any?
|
|
147
|
-
end
|
|
148
|
-
|
|
149
|
-
# Normalizes category names for comparison
|
|
150
|
-
#
|
|
151
|
-
# @param category [String, Symbol] Category name
|
|
152
|
-
# @return [Symbol] Normalized category symbol
|
|
153
|
-
def normalize_category(category)
|
|
154
|
-
category.to_s.tr("/", "_").tr("-", "_").downcase.to_sym
|
|
155
|
-
end
|
|
156
|
-
end
|
|
157
|
-
end
|
|
158
|
-
end
|
|
@@ -1,237 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require_relative "../results/moderation_result"
|
|
4
|
-
|
|
5
|
-
module RubyLLM
|
|
6
|
-
module Agents
|
|
7
|
-
# Standalone moderator for content moderation using the middleware pipeline
|
|
8
|
-
#
|
|
9
|
-
# Provides a class-based interface for moderating content with built-in
|
|
10
|
-
# support for caching, instrumentation, and multi-tenancy through the
|
|
11
|
-
# middleware pipeline.
|
|
12
|
-
#
|
|
13
|
-
# @example Basic usage
|
|
14
|
-
# class ContentModerator < RubyLLM::Agents::Moderator
|
|
15
|
-
# model 'omni-moderation-latest'
|
|
16
|
-
# end
|
|
17
|
-
#
|
|
18
|
-
# result = ContentModerator.call(text: "content to check")
|
|
19
|
-
# result.flagged? # => true/false
|
|
20
|
-
#
|
|
21
|
-
# @example With configuration
|
|
22
|
-
# class StrictModerator < RubyLLM::Agents::Moderator
|
|
23
|
-
# model 'omni-moderation-latest'
|
|
24
|
-
# threshold 0.7
|
|
25
|
-
# categories :hate, :violence, :harassment
|
|
26
|
-
# end
|
|
27
|
-
#
|
|
28
|
-
# @example Runtime override
|
|
29
|
-
# result = ContentModerator.call(
|
|
30
|
-
# text: "content",
|
|
31
|
-
# threshold: 0.9,
|
|
32
|
-
# categories: [:hate]
|
|
33
|
-
# )
|
|
34
|
-
#
|
|
35
|
-
# @api public
|
|
36
|
-
class Moderator < BaseAgent
|
|
37
|
-
class << self
|
|
38
|
-
# Returns the agent type for moderators
|
|
39
|
-
#
|
|
40
|
-
# @return [Symbol] :moderation
|
|
41
|
-
def agent_type
|
|
42
|
-
:moderation
|
|
43
|
-
end
|
|
44
|
-
|
|
45
|
-
# @!group Moderation-specific DSL
|
|
46
|
-
|
|
47
|
-
# Sets or returns the moderation model
|
|
48
|
-
#
|
|
49
|
-
# Defaults to the moderation model from configuration, not the
|
|
50
|
-
# conversation model that BaseAgent uses.
|
|
51
|
-
#
|
|
52
|
-
# @param value [String, nil] Model identifier to set
|
|
53
|
-
# @return [String] Current model setting
|
|
54
|
-
def model(value = nil)
|
|
55
|
-
@model = value if value
|
|
56
|
-
return @model if defined?(@model) && @model
|
|
57
|
-
|
|
58
|
-
# For inheritance: check if parent is also a Moderator
|
|
59
|
-
if superclass.respond_to?(:agent_type) && superclass.agent_type == :moderation
|
|
60
|
-
superclass.model
|
|
61
|
-
else
|
|
62
|
-
default_moderation_model
|
|
63
|
-
end
|
|
64
|
-
end
|
|
65
|
-
|
|
66
|
-
# Sets or returns the score threshold
|
|
67
|
-
#
|
|
68
|
-
# @param value [Float, nil] Threshold value (0.0-1.0)
|
|
69
|
-
# @return [Float, nil] Current threshold
|
|
70
|
-
def threshold(value = nil)
|
|
71
|
-
@threshold = value if value
|
|
72
|
-
@threshold || inherited_or_default(:threshold, nil)
|
|
73
|
-
end
|
|
74
|
-
|
|
75
|
-
# Sets or returns the categories to check
|
|
76
|
-
#
|
|
77
|
-
# @param cats [Array<Symbol>] Category symbols
|
|
78
|
-
# @return [Array<Symbol>, nil] Current categories
|
|
79
|
-
def categories(*cats)
|
|
80
|
-
@categories = cats.flatten.map(&:to_sym) if cats.any?
|
|
81
|
-
@categories || inherited_or_default(:categories, nil)
|
|
82
|
-
end
|
|
83
|
-
|
|
84
|
-
# @!endgroup
|
|
85
|
-
|
|
86
|
-
# Factory method to instantiate and execute moderation
|
|
87
|
-
#
|
|
88
|
-
# @param text [String] Text to moderate
|
|
89
|
-
# @param options [Hash] Runtime options
|
|
90
|
-
# @option options [String] :model Override moderation model
|
|
91
|
-
# @option options [Float] :threshold Override threshold
|
|
92
|
-
# @option options [Array<Symbol>] :categories Override categories
|
|
93
|
-
# @option options [Object] :tenant Tenant for multi-tenancy
|
|
94
|
-
# @return [ModerationResult] The moderation result
|
|
95
|
-
def call(text:, **options)
|
|
96
|
-
new(text: text, **options).call
|
|
97
|
-
end
|
|
98
|
-
|
|
99
|
-
private
|
|
100
|
-
|
|
101
|
-
def inherited_or_default(method, default)
|
|
102
|
-
superclass.respond_to?(method) ? superclass.send(method) : default
|
|
103
|
-
end
|
|
104
|
-
|
|
105
|
-
def default_moderation_model
|
|
106
|
-
RubyLLM::Agents.configuration.default_moderation_model
|
|
107
|
-
rescue StandardError
|
|
108
|
-
"omni-moderation-latest"
|
|
109
|
-
end
|
|
110
|
-
end
|
|
111
|
-
|
|
112
|
-
# @!attribute [r] text
|
|
113
|
-
# @return [String] Text to moderate
|
|
114
|
-
attr_reader :text
|
|
115
|
-
|
|
116
|
-
# Creates a new Moderator instance
|
|
117
|
-
#
|
|
118
|
-
# @param text [String] Text to moderate
|
|
119
|
-
# @param options [Hash] Runtime options
|
|
120
|
-
def initialize(text:, **options)
|
|
121
|
-
@text = text
|
|
122
|
-
@runtime_threshold = options.delete(:threshold)
|
|
123
|
-
@runtime_categories = options.delete(:categories)
|
|
124
|
-
|
|
125
|
-
# Set model to moderation model if not specified
|
|
126
|
-
options[:model] ||= self.class.model
|
|
127
|
-
|
|
128
|
-
super(**options)
|
|
129
|
-
end
|
|
130
|
-
|
|
131
|
-
# Executes the moderation through the middleware pipeline
|
|
132
|
-
#
|
|
133
|
-
# @return [ModerationResult] The moderation result
|
|
134
|
-
def call
|
|
135
|
-
context = build_context
|
|
136
|
-
result_context = Pipeline::Executor.execute(context)
|
|
137
|
-
result_context.output
|
|
138
|
-
end
|
|
139
|
-
|
|
140
|
-
# The input for this moderation operation
|
|
141
|
-
#
|
|
142
|
-
# Used by the pipeline to generate cache keys and for instrumentation.
|
|
143
|
-
#
|
|
144
|
-
# @return [String] The text being moderated
|
|
145
|
-
def user_prompt
|
|
146
|
-
text
|
|
147
|
-
end
|
|
148
|
-
|
|
149
|
-
# Core moderation execution
|
|
150
|
-
#
|
|
151
|
-
# This is called by the Pipeline::Executor after middleware
|
|
152
|
-
# has been applied. Only contains the moderation API logic.
|
|
153
|
-
#
|
|
154
|
-
# @param context [Pipeline::Context] The execution context
|
|
155
|
-
# @return [void] Sets context.output with the ModerationResult
|
|
156
|
-
def execute(context)
|
|
157
|
-
# Track timing internally
|
|
158
|
-
execution_started_at = Time.current
|
|
159
|
-
|
|
160
|
-
moderation_opts = {}
|
|
161
|
-
moderation_opts[:model] = resolved_model if resolved_model
|
|
162
|
-
|
|
163
|
-
raw_result = RubyLLM.moderate(text, **moderation_opts)
|
|
164
|
-
|
|
165
|
-
execution_completed_at = Time.current
|
|
166
|
-
duration_ms = ((execution_completed_at - execution_started_at) * 1000).to_i
|
|
167
|
-
|
|
168
|
-
# Update context with basic info (no tokens for moderation)
|
|
169
|
-
context.input_tokens = 0
|
|
170
|
-
context.output_tokens = 0
|
|
171
|
-
context.total_cost = 0.0
|
|
172
|
-
|
|
173
|
-
# Build final result
|
|
174
|
-
context.output = ModerationResult.new(
|
|
175
|
-
result: raw_result,
|
|
176
|
-
threshold: resolved_threshold,
|
|
177
|
-
categories: resolved_categories
|
|
178
|
-
)
|
|
179
|
-
end
|
|
180
|
-
|
|
181
|
-
# Generates the cache key for this moderation
|
|
182
|
-
#
|
|
183
|
-
# @return [String] Cache key in format "ruby_llm_agents/moderation/..."
|
|
184
|
-
def agent_cache_key
|
|
185
|
-
components = [
|
|
186
|
-
"ruby_llm_agents",
|
|
187
|
-
"moderation",
|
|
188
|
-
self.class.name,
|
|
189
|
-
self.class.version,
|
|
190
|
-
resolved_model,
|
|
191
|
-
resolved_threshold,
|
|
192
|
-
resolved_categories&.sort&.join(","),
|
|
193
|
-
Digest::SHA256.hexdigest(text)
|
|
194
|
-
].compact
|
|
195
|
-
|
|
196
|
-
components.join("/")
|
|
197
|
-
end
|
|
198
|
-
|
|
199
|
-
private
|
|
200
|
-
|
|
201
|
-
# Builds context for pipeline execution
|
|
202
|
-
#
|
|
203
|
-
# @return [Pipeline::Context] The context object
|
|
204
|
-
def build_context
|
|
205
|
-
Pipeline::Context.new(
|
|
206
|
-
input: user_prompt,
|
|
207
|
-
agent_class: self.class,
|
|
208
|
-
agent_instance: self,
|
|
209
|
-
model: resolved_model,
|
|
210
|
-
tenant: @options[:tenant],
|
|
211
|
-
skip_cache: @options[:skip_cache]
|
|
212
|
-
)
|
|
213
|
-
end
|
|
214
|
-
|
|
215
|
-
# Resolves the model to use
|
|
216
|
-
#
|
|
217
|
-
# @return [String] The model identifier
|
|
218
|
-
def resolved_model
|
|
219
|
-
@model || self.class.model
|
|
220
|
-
end
|
|
221
|
-
|
|
222
|
-
# Resolves the threshold to use
|
|
223
|
-
#
|
|
224
|
-
# @return [Float, nil] The threshold
|
|
225
|
-
def resolved_threshold
|
|
226
|
-
@runtime_threshold || self.class.threshold
|
|
227
|
-
end
|
|
228
|
-
|
|
229
|
-
# Resolves the categories to check
|
|
230
|
-
#
|
|
231
|
-
# @return [Array<Symbol>, nil] The categories
|
|
232
|
-
def resolved_categories
|
|
233
|
-
@runtime_categories || self.class.categories
|
|
234
|
-
end
|
|
235
|
-
end
|
|
236
|
-
end
|
|
237
|
-
end
|