ruby_llm-agents 0.4.0 → 1.0.0.beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +225 -34
- data/app/controllers/ruby_llm/agents/agents_controller.rb +136 -16
- data/app/controllers/ruby_llm/agents/api_configurations_controller.rb +214 -0
- data/app/controllers/ruby_llm/agents/dashboard_controller.rb +29 -9
- data/app/controllers/ruby_llm/agents/{settings_controller.rb → system_config_controller.rb} +3 -3
- data/app/controllers/ruby_llm/agents/tenants_controller.rb +109 -0
- data/app/controllers/ruby_llm/agents/workflows_controller.rb +355 -0
- data/app/helpers/ruby_llm/agents/application_helper.rb +25 -0
- data/app/models/ruby_llm/agents/api_configuration.rb +386 -0
- data/app/models/ruby_llm/agents/execution.rb +3 -0
- data/app/models/ruby_llm/agents/tenant_budget.rb +112 -14
- data/app/services/ruby_llm/agents/agent_registry.rb +51 -12
- data/app/views/layouts/ruby_llm/agents/application.html.erb +5 -30
- data/app/views/ruby_llm/agents/agents/_agent.html.erb +13 -1
- data/app/views/ruby_llm/agents/agents/_config_agent.html.erb +235 -0
- data/app/views/ruby_llm/agents/agents/_config_embedder.html.erb +70 -0
- data/app/views/ruby_llm/agents/agents/_config_image_generator.html.erb +152 -0
- data/app/views/ruby_llm/agents/agents/_config_moderator.html.erb +63 -0
- data/app/views/ruby_llm/agents/agents/_config_speaker.html.erb +108 -0
- data/app/views/ruby_llm/agents/agents/_config_transcriber.html.erb +91 -0
- data/app/views/ruby_llm/agents/agents/_workflow.html.erb +1 -1
- data/app/views/ruby_llm/agents/agents/index.html.erb +74 -9
- data/app/views/ruby_llm/agents/agents/show.html.erb +18 -378
- data/app/views/ruby_llm/agents/api_configurations/_api_key_field.html.erb +34 -0
- data/app/views/ruby_llm/agents/api_configurations/_form.html.erb +288 -0
- data/app/views/ruby_llm/agents/api_configurations/edit.html.erb +95 -0
- data/app/views/ruby_llm/agents/api_configurations/edit_tenant.html.erb +97 -0
- data/app/views/ruby_llm/agents/api_configurations/show.html.erb +211 -0
- data/app/views/ruby_llm/agents/api_configurations/tenant.html.erb +179 -0
- data/app/views/ruby_llm/agents/dashboard/_action_center.html.erb +1 -1
- data/app/views/ruby_llm/agents/dashboard/_agent_comparison.html.erb +269 -15
- data/app/views/ruby_llm/agents/executions/show.html.erb +98 -0
- data/app/views/ruby_llm/agents/shared/_agent_type_badge.html.erb +93 -0
- data/app/views/ruby_llm/agents/{settings → system_config}/show.html.erb +1 -1
- data/app/views/ruby_llm/agents/tenants/_form.html.erb +150 -0
- data/app/views/ruby_llm/agents/tenants/edit.html.erb +13 -0
- data/app/views/ruby_llm/agents/tenants/index.html.erb +129 -0
- data/app/views/ruby_llm/agents/tenants/show.html.erb +374 -0
- data/app/views/ruby_llm/agents/workflows/_step_performance.html.erb +236 -0
- data/app/views/ruby_llm/agents/workflows/_structure_parallel.html.erb +76 -0
- data/app/views/ruby_llm/agents/workflows/_structure_pipeline.html.erb +74 -0
- data/app/views/ruby_llm/agents/workflows/_structure_router.html.erb +108 -0
- data/app/views/ruby_llm/agents/workflows/show.html.erb +442 -0
- data/config/routes.rb +13 -1
- data/lib/generators/ruby_llm_agents/agent_generator.rb +56 -7
- data/lib/generators/ruby_llm_agents/api_configuration_generator.rb +100 -0
- data/lib/generators/ruby_llm_agents/background_remover_generator.rb +110 -0
- data/lib/generators/ruby_llm_agents/embedder_generator.rb +107 -0
- data/lib/generators/ruby_llm_agents/image_analyzer_generator.rb +115 -0
- data/lib/generators/ruby_llm_agents/image_editor_generator.rb +108 -0
- data/lib/generators/ruby_llm_agents/image_generator_generator.rb +116 -0
- data/lib/generators/ruby_llm_agents/image_pipeline_generator.rb +178 -0
- data/lib/generators/ruby_llm_agents/image_transformer_generator.rb +109 -0
- data/lib/generators/ruby_llm_agents/image_upscaler_generator.rb +103 -0
- data/lib/generators/ruby_llm_agents/image_variator_generator.rb +102 -0
- data/lib/generators/ruby_llm_agents/install_generator.rb +76 -4
- data/lib/generators/ruby_llm_agents/restructure_generator.rb +292 -0
- data/lib/generators/ruby_llm_agents/speaker_generator.rb +121 -0
- data/lib/generators/ruby_llm_agents/templates/add_execution_type_migration.rb.tt +8 -0
- data/lib/generators/ruby_llm_agents/templates/agent.rb.tt +99 -84
- data/lib/generators/ruby_llm_agents/templates/application_agent.rb.tt +42 -40
- data/lib/generators/ruby_llm_agents/templates/application_background_remover.rb.tt +26 -0
- data/lib/generators/ruby_llm_agents/templates/application_embedder.rb.tt +50 -0
- data/lib/generators/ruby_llm_agents/templates/application_image_analyzer.rb.tt +26 -0
- data/lib/generators/ruby_llm_agents/templates/application_image_editor.rb.tt +20 -0
- data/lib/generators/ruby_llm_agents/templates/application_image_generator.rb.tt +38 -0
- data/lib/generators/ruby_llm_agents/templates/application_image_pipeline.rb.tt +139 -0
- data/lib/generators/ruby_llm_agents/templates/application_image_transformer.rb.tt +21 -0
- data/lib/generators/ruby_llm_agents/templates/application_image_upscaler.rb.tt +20 -0
- data/lib/generators/ruby_llm_agents/templates/application_image_variator.rb.tt +20 -0
- data/lib/generators/ruby_llm_agents/templates/application_speaker.rb.tt +49 -0
- data/lib/generators/ruby_llm_agents/templates/application_transcriber.rb.tt +53 -0
- data/lib/generators/ruby_llm_agents/templates/background_remover.rb.tt +44 -0
- data/lib/generators/ruby_llm_agents/templates/create_api_configurations_migration.rb.tt +90 -0
- data/lib/generators/ruby_llm_agents/templates/embedder.rb.tt +41 -0
- data/lib/generators/ruby_llm_agents/templates/image_analyzer.rb.tt +45 -0
- data/lib/generators/ruby_llm_agents/templates/image_editor.rb.tt +35 -0
- data/lib/generators/ruby_llm_agents/templates/image_generator.rb.tt +47 -0
- data/lib/generators/ruby_llm_agents/templates/image_pipeline.rb.tt +50 -0
- data/lib/generators/ruby_llm_agents/templates/image_transformer.rb.tt +44 -0
- data/lib/generators/ruby_llm_agents/templates/image_upscaler.rb.tt +38 -0
- data/lib/generators/ruby_llm_agents/templates/image_variator.rb.tt +33 -0
- data/lib/generators/ruby_llm_agents/templates/skills/AGENTS.md.tt +228 -0
- data/lib/generators/ruby_llm_agents/templates/skills/BACKGROUND_REMOVERS.md.tt +131 -0
- data/lib/generators/ruby_llm_agents/templates/skills/EMBEDDERS.md.tt +255 -0
- data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_ANALYZERS.md.tt +120 -0
- data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_EDITORS.md.tt +102 -0
- data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_GENERATORS.md.tt +282 -0
- data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_PIPELINES.md.tt +228 -0
- data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_TRANSFORMERS.md.tt +120 -0
- data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_UPSCALERS.md.tt +110 -0
- data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_VARIATORS.md.tt +120 -0
- data/lib/generators/ruby_llm_agents/templates/skills/SPEAKERS.md.tt +212 -0
- data/lib/generators/ruby_llm_agents/templates/skills/TOOLS.md.tt +227 -0
- data/lib/generators/ruby_llm_agents/templates/skills/TRANSCRIBERS.md.tt +251 -0
- data/lib/generators/ruby_llm_agents/templates/skills/WORKFLOWS.md.tt +300 -0
- data/lib/generators/ruby_llm_agents/templates/speaker.rb.tt +56 -0
- data/lib/generators/ruby_llm_agents/templates/transcriber.rb.tt +51 -0
- data/lib/generators/ruby_llm_agents/transcriber_generator.rb +107 -0
- data/lib/generators/ruby_llm_agents/upgrade_generator.rb +152 -1
- data/lib/ruby_llm/agents/audio/speaker.rb +553 -0
- data/lib/ruby_llm/agents/audio/transcriber.rb +669 -0
- data/lib/ruby_llm/agents/base_agent.rb +675 -0
- data/lib/ruby_llm/agents/core/base/moderation_dsl.rb +181 -0
- data/lib/ruby_llm/agents/core/base/moderation_execution.rb +274 -0
- data/lib/ruby_llm/agents/core/base.rb +135 -0
- data/lib/ruby_llm/agents/core/configuration.rb +981 -0
- data/lib/ruby_llm/agents/core/errors.rb +150 -0
- data/lib/ruby_llm/agents/{instrumentation.rb → core/instrumentation.rb} +93 -4
- data/lib/ruby_llm/agents/core/llm_tenant.rb +358 -0
- data/lib/ruby_llm/agents/core/resolved_config.rb +348 -0
- data/lib/ruby_llm/agents/{version.rb → core/version.rb} +1 -1
- data/lib/ruby_llm/agents/dsl/base.rb +110 -0
- data/lib/ruby_llm/agents/dsl/caching.rb +142 -0
- data/lib/ruby_llm/agents/dsl/reliability.rb +307 -0
- data/lib/ruby_llm/agents/dsl.rb +41 -0
- data/lib/ruby_llm/agents/image/analyzer/dsl.rb +130 -0
- data/lib/ruby_llm/agents/image/analyzer/execution.rb +402 -0
- data/lib/ruby_llm/agents/image/analyzer.rb +90 -0
- data/lib/ruby_llm/agents/image/background_remover/dsl.rb +154 -0
- data/lib/ruby_llm/agents/image/background_remover/execution.rb +240 -0
- data/lib/ruby_llm/agents/image/background_remover.rb +89 -0
- data/lib/ruby_llm/agents/image/concerns/image_operation_dsl.rb +91 -0
- data/lib/ruby_llm/agents/image/concerns/image_operation_execution.rb +165 -0
- data/lib/ruby_llm/agents/image/editor/dsl.rb +56 -0
- data/lib/ruby_llm/agents/image/editor/execution.rb +207 -0
- data/lib/ruby_llm/agents/image/editor.rb +92 -0
- data/lib/ruby_llm/agents/image/generator/active_storage_support.rb +127 -0
- data/lib/ruby_llm/agents/image/generator/content_policy.rb +95 -0
- data/lib/ruby_llm/agents/image/generator/pricing.rb +353 -0
- data/lib/ruby_llm/agents/image/generator/templates.rb +124 -0
- data/lib/ruby_llm/agents/image/generator.rb +455 -0
- data/lib/ruby_llm/agents/image/pipeline/dsl.rb +213 -0
- data/lib/ruby_llm/agents/image/pipeline/execution.rb +382 -0
- data/lib/ruby_llm/agents/image/pipeline.rb +97 -0
- data/lib/ruby_llm/agents/image/transformer/dsl.rb +148 -0
- data/lib/ruby_llm/agents/image/transformer/execution.rb +223 -0
- data/lib/ruby_llm/agents/image/transformer.rb +95 -0
- data/lib/ruby_llm/agents/image/upscaler/dsl.rb +83 -0
- data/lib/ruby_llm/agents/image/upscaler/execution.rb +219 -0
- data/lib/ruby_llm/agents/image/upscaler.rb +81 -0
- data/lib/ruby_llm/agents/image/variator/dsl.rb +62 -0
- data/lib/ruby_llm/agents/image/variator/execution.rb +189 -0
- data/lib/ruby_llm/agents/image/variator.rb +80 -0
- data/lib/ruby_llm/agents/{alert_manager.rb → infrastructure/alert_manager.rb} +17 -22
- data/lib/ruby_llm/agents/infrastructure/budget/budget_query.rb +145 -0
- data/lib/ruby_llm/agents/infrastructure/budget/config_resolver.rb +149 -0
- data/lib/ruby_llm/agents/infrastructure/budget/forecaster.rb +68 -0
- data/lib/ruby_llm/agents/infrastructure/budget/spend_recorder.rb +279 -0
- data/lib/ruby_llm/agents/infrastructure/budget_tracker.rb +275 -0
- data/lib/ruby_llm/agents/{execution_logger_job.rb → infrastructure/execution_logger_job.rb} +17 -1
- data/lib/ruby_llm/agents/{reliability → infrastructure/reliability}/executor.rb +2 -1
- data/lib/ruby_llm/agents/{reliability → infrastructure/reliability}/retry_strategy.rb +9 -3
- data/lib/ruby_llm/agents/{reliability.rb → infrastructure/reliability.rb} +11 -21
- data/lib/ruby_llm/agents/pipeline/builder.rb +215 -0
- data/lib/ruby_llm/agents/pipeline/context.rb +255 -0
- data/lib/ruby_llm/agents/pipeline/executor.rb +86 -0
- data/lib/ruby_llm/agents/pipeline/middleware/base.rb +124 -0
- data/lib/ruby_llm/agents/pipeline/middleware/budget.rb +95 -0
- data/lib/ruby_llm/agents/pipeline/middleware/cache.rb +171 -0
- data/lib/ruby_llm/agents/pipeline/middleware/instrumentation.rb +415 -0
- data/lib/ruby_llm/agents/pipeline/middleware/reliability.rb +276 -0
- data/lib/ruby_llm/agents/pipeline/middleware/tenant.rb +196 -0
- data/lib/ruby_llm/agents/pipeline.rb +68 -0
- data/lib/ruby_llm/agents/{engine.rb → rails/engine.rb} +79 -10
- data/lib/ruby_llm/agents/results/background_removal_result.rb +286 -0
- data/lib/ruby_llm/agents/{result.rb → results/base.rb} +73 -1
- data/lib/ruby_llm/agents/results/embedding_result.rb +243 -0
- data/lib/ruby_llm/agents/results/image_analysis_result.rb +314 -0
- data/lib/ruby_llm/agents/results/image_edit_result.rb +250 -0
- data/lib/ruby_llm/agents/results/image_generation_result.rb +346 -0
- data/lib/ruby_llm/agents/results/image_pipeline_result.rb +399 -0
- data/lib/ruby_llm/agents/results/image_transform_result.rb +251 -0
- data/lib/ruby_llm/agents/results/image_upscale_result.rb +255 -0
- data/lib/ruby_llm/agents/results/image_variation_result.rb +237 -0
- data/lib/ruby_llm/agents/results/moderation_result.rb +158 -0
- data/lib/ruby_llm/agents/results/speech_result.rb +338 -0
- data/lib/ruby_llm/agents/results/transcription_result.rb +408 -0
- data/lib/ruby_llm/agents/text/embedder.rb +444 -0
- data/lib/ruby_llm/agents/text/moderator.rb +237 -0
- data/lib/ruby_llm/agents/workflow/async.rb +220 -0
- data/lib/ruby_llm/agents/workflow/async_executor.rb +156 -0
- data/lib/ruby_llm/agents/{workflow.rb → workflow/orchestrator.rb} +6 -5
- data/lib/ruby_llm/agents/workflow/parallel.rb +34 -17
- data/lib/ruby_llm/agents/workflow/thread_pool.rb +185 -0
- data/lib/ruby_llm/agents.rb +86 -20
- metadata +189 -35
- data/lib/ruby_llm/agents/base/caching.rb +0 -40
- data/lib/ruby_llm/agents/base/cost_calculation.rb +0 -105
- data/lib/ruby_llm/agents/base/dsl.rb +0 -324
- data/lib/ruby_llm/agents/base/execution.rb +0 -283
- data/lib/ruby_llm/agents/base/reliability_dsl.rb +0 -82
- data/lib/ruby_llm/agents/base/reliability_execution.rb +0 -136
- data/lib/ruby_llm/agents/base/response_building.rb +0 -86
- data/lib/ruby_llm/agents/base/tool_tracking.rb +0 -57
- data/lib/ruby_llm/agents/base.rb +0 -209
- data/lib/ruby_llm/agents/budget_tracker.rb +0 -471
- data/lib/ruby_llm/agents/configuration.rb +0 -357
- /data/lib/ruby_llm/agents/{deprecations.rb → core/deprecations.rb} +0 -0
- /data/lib/ruby_llm/agents/{inflections.rb → core/inflections.rb} +0 -0
- /data/lib/ruby_llm/agents/{attempt_tracker.rb → infrastructure/attempt_tracker.rb} +0 -0
- /data/lib/ruby_llm/agents/{cache_helper.rb → infrastructure/cache_helper.rb} +0 -0
- /data/lib/ruby_llm/agents/{circuit_breaker.rb → infrastructure/circuit_breaker.rb} +0 -0
- /data/lib/ruby_llm/agents/{redactor.rb → infrastructure/redactor.rb} +0 -0
- /data/lib/ruby_llm/agents/{reliability → infrastructure/reliability}/breaker_manager.rb +0 -0
- /data/lib/ruby_llm/agents/{reliability → infrastructure/reliability}/execution_constraints.rb +0 -0
- /data/lib/ruby_llm/agents/{reliability → infrastructure/reliability}/fallback_routing.rb +0 -0
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
module Agents
|
|
5
|
+
# DSL for configuring content moderation on agents
|
|
6
|
+
#
|
|
7
|
+
# Provides declarative configuration for moderating user input
|
|
8
|
+
# and/or LLM output against safety policies.
|
|
9
|
+
#
|
|
10
|
+
# @example Basic input moderation
|
|
11
|
+
# class MyAgent < ApplicationAgent
|
|
12
|
+
# moderation :input
|
|
13
|
+
# end
|
|
14
|
+
#
|
|
15
|
+
# @example Moderate both input and output
|
|
16
|
+
# class MyAgent < ApplicationAgent
|
|
17
|
+
# moderation :both
|
|
18
|
+
# end
|
|
19
|
+
#
|
|
20
|
+
# @example With configuration options
|
|
21
|
+
# class MyAgent < ApplicationAgent
|
|
22
|
+
# moderation :input,
|
|
23
|
+
# model: 'omni-moderation-latest',
|
|
24
|
+
# threshold: 0.8,
|
|
25
|
+
# categories: [:hate, :violence],
|
|
26
|
+
# on_flagged: :raise
|
|
27
|
+
# end
|
|
28
|
+
#
|
|
29
|
+
# @example Block-based DSL
|
|
30
|
+
# class MyAgent < ApplicationAgent
|
|
31
|
+
# moderation do
|
|
32
|
+
# input enabled: true, threshold: 0.7
|
|
33
|
+
# output enabled: true, threshold: 0.9
|
|
34
|
+
# model 'omni-moderation-latest'
|
|
35
|
+
# categories :hate, :violence
|
|
36
|
+
# on_flagged :block
|
|
37
|
+
# end
|
|
38
|
+
# end
|
|
39
|
+
#
|
|
40
|
+
# @api public
|
|
41
|
+
module ModerationDSL
|
|
42
|
+
# Configures content moderation for this agent
|
|
43
|
+
#
|
|
44
|
+
# @param phases [Array<Symbol>] Phases to moderate (:input, :output, :both)
|
|
45
|
+
# @param model [String] Moderation model to use
|
|
46
|
+
# @param threshold [Float] Score threshold (0.0-1.0) for flagging
|
|
47
|
+
# @param categories [Array<Symbol>] Categories to check
|
|
48
|
+
# @param on_flagged [Symbol] Action when flagged (:block, :raise, :warn, :log)
|
|
49
|
+
# @param custom_handler [Symbol] Method name for custom handling
|
|
50
|
+
# @yield Block for advanced configuration
|
|
51
|
+
# @return [Hash, nil] The moderation configuration
|
|
52
|
+
#
|
|
53
|
+
# @example Simple input moderation
|
|
54
|
+
# moderation :input
|
|
55
|
+
#
|
|
56
|
+
# @example Input and output moderation
|
|
57
|
+
# moderation :input, :output
|
|
58
|
+
# # or
|
|
59
|
+
# moderation :both
|
|
60
|
+
#
|
|
61
|
+
# @example With options
|
|
62
|
+
# moderation :input, threshold: 0.8, on_flagged: :raise
|
|
63
|
+
def moderation(*phases, **options, &block)
|
|
64
|
+
if block_given?
|
|
65
|
+
builder = ModerationBuilder.new
|
|
66
|
+
builder.instance_eval(&block)
|
|
67
|
+
@moderation_config = builder.config
|
|
68
|
+
else
|
|
69
|
+
# Handle :both shorthand
|
|
70
|
+
phases = [:input, :output] if phases.include?(:both)
|
|
71
|
+
phases = [:input] if phases.empty?
|
|
72
|
+
|
|
73
|
+
@moderation_config = {
|
|
74
|
+
phases: phases.flatten.map(&:to_sym),
|
|
75
|
+
model: options[:model],
|
|
76
|
+
threshold: options[:threshold],
|
|
77
|
+
categories: options[:categories],
|
|
78
|
+
on_flagged: options[:on_flagged] || :block,
|
|
79
|
+
custom_handler: options[:custom_handler]
|
|
80
|
+
}
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Returns the moderation configuration for this agent
|
|
85
|
+
#
|
|
86
|
+
# @return [Hash, nil] The moderation configuration or nil if not configured
|
|
87
|
+
def moderation_config
|
|
88
|
+
@moderation_config || inherited_or_default(:moderation_config, nil)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Returns whether moderation is enabled for this agent
|
|
92
|
+
#
|
|
93
|
+
# @return [Boolean] true if moderation is configured
|
|
94
|
+
def moderation_enabled?
|
|
95
|
+
!!moderation_config
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
private
|
|
99
|
+
|
|
100
|
+
def inherited_or_default(method, default)
|
|
101
|
+
return default unless superclass.respond_to?(method)
|
|
102
|
+
|
|
103
|
+
superclass.send(method)
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# Builder class for block-based moderation configuration
|
|
108
|
+
#
|
|
109
|
+
# @api private
|
|
110
|
+
class ModerationBuilder
|
|
111
|
+
attr_reader :config
|
|
112
|
+
|
|
113
|
+
def initialize
|
|
114
|
+
@config = {
|
|
115
|
+
phases: [],
|
|
116
|
+
on_flagged: :block
|
|
117
|
+
}
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# Enables input moderation
|
|
121
|
+
#
|
|
122
|
+
# @param enabled [Boolean] Whether to enable input moderation
|
|
123
|
+
# @param threshold [Float, nil] Score threshold for input phase
|
|
124
|
+
# @return [void]
|
|
125
|
+
def input(enabled: true, threshold: nil)
|
|
126
|
+
@config[:phases] << :input if enabled
|
|
127
|
+
@config[:input_threshold] = threshold if threshold
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# Enables output moderation
|
|
131
|
+
#
|
|
132
|
+
# @param enabled [Boolean] Whether to enable output moderation
|
|
133
|
+
# @param threshold [Float, nil] Score threshold for output phase
|
|
134
|
+
# @return [void]
|
|
135
|
+
def output(enabled: true, threshold: nil)
|
|
136
|
+
@config[:phases] << :output if enabled
|
|
137
|
+
@config[:output_threshold] = threshold if threshold
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# Sets the moderation model
|
|
141
|
+
#
|
|
142
|
+
# @param model_name [String] Model identifier
|
|
143
|
+
# @return [void]
|
|
144
|
+
def model(model_name)
|
|
145
|
+
@config[:model] = model_name
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# Sets the global threshold
|
|
149
|
+
#
|
|
150
|
+
# @param value [Float] Score threshold (0.0-1.0)
|
|
151
|
+
# @return [void]
|
|
152
|
+
def threshold(value)
|
|
153
|
+
@config[:threshold] = value
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# Sets categories to check
|
|
157
|
+
#
|
|
158
|
+
# @param cats [Array<Symbol>] Category symbols
|
|
159
|
+
# @return [void]
|
|
160
|
+
def categories(*cats)
|
|
161
|
+
@config[:categories] = cats.flatten.map(&:to_sym)
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
# Sets the action when content is flagged
|
|
165
|
+
#
|
|
166
|
+
# @param action [Symbol] :block, :raise, :warn, or :log
|
|
167
|
+
# @return [void]
|
|
168
|
+
def on_flagged(action)
|
|
169
|
+
@config[:on_flagged] = action
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
# Sets a custom handler method
|
|
173
|
+
#
|
|
174
|
+
# @param method_name [Symbol] Method name to call on the agent
|
|
175
|
+
# @return [void]
|
|
176
|
+
def custom_handler(method_name)
|
|
177
|
+
@config[:custom_handler] = method_name
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
end
|
|
@@ -0,0 +1,274 @@
|
|
|
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
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "base/moderation_dsl"
|
|
4
|
+
require_relative "base/moderation_execution"
|
|
5
|
+
|
|
6
|
+
module RubyLLM
|
|
7
|
+
module Agents
|
|
8
|
+
# Base class for LLM-powered conversational agents
|
|
9
|
+
#
|
|
10
|
+
# Inherits from BaseAgent to use the middleware pipeline architecture
|
|
11
|
+
# while adding moderation capabilities for input/output content filtering.
|
|
12
|
+
#
|
|
13
|
+
# @example Creating an agent
|
|
14
|
+
# class SearchAgent < ApplicationAgent
|
|
15
|
+
# model "gpt-4o"
|
|
16
|
+
# temperature 0.0
|
|
17
|
+
# version "1.0"
|
|
18
|
+
# timeout 30
|
|
19
|
+
# cache_for 1.hour
|
|
20
|
+
#
|
|
21
|
+
# param :query, required: true
|
|
22
|
+
# param :limit, default: 10
|
|
23
|
+
#
|
|
24
|
+
# def system_prompt
|
|
25
|
+
# "You are a search assistant..."
|
|
26
|
+
# end
|
|
27
|
+
#
|
|
28
|
+
# def user_prompt
|
|
29
|
+
# "Search for: #{query}"
|
|
30
|
+
# end
|
|
31
|
+
# end
|
|
32
|
+
#
|
|
33
|
+
# @example With moderation
|
|
34
|
+
# class SafeAgent < ApplicationAgent
|
|
35
|
+
# moderation :input, :output
|
|
36
|
+
# # or
|
|
37
|
+
# moderation :both
|
|
38
|
+
#
|
|
39
|
+
# def user_prompt
|
|
40
|
+
# query
|
|
41
|
+
# end
|
|
42
|
+
# end
|
|
43
|
+
#
|
|
44
|
+
# @example Calling an agent
|
|
45
|
+
# SearchAgent.call(query: "red dress")
|
|
46
|
+
# SearchAgent.call(query: "red dress", dry_run: true) # Debug mode
|
|
47
|
+
# SearchAgent.call(query: "red dress", skip_cache: true) # Bypass cache
|
|
48
|
+
#
|
|
49
|
+
# @see RubyLLM::Agents::BaseAgent
|
|
50
|
+
# @api public
|
|
51
|
+
class Base < BaseAgent
|
|
52
|
+
extend ModerationDSL
|
|
53
|
+
include ModerationExecution
|
|
54
|
+
|
|
55
|
+
class << self
|
|
56
|
+
# Returns the agent type for conversation agents
|
|
57
|
+
#
|
|
58
|
+
# @return [Symbol] :conversation
|
|
59
|
+
def agent_type
|
|
60
|
+
:conversation
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Execute the core LLM call with moderation support
|
|
65
|
+
#
|
|
66
|
+
# This extends BaseAgent's execute method to add input and output
|
|
67
|
+
# moderation checks when configured via the moderation DSL.
|
|
68
|
+
#
|
|
69
|
+
# @param context [Pipeline::Context] The execution context
|
|
70
|
+
# @return [void] Sets context.output with the result
|
|
71
|
+
def execute(context)
|
|
72
|
+
@execution_started_at = context.started_at || Time.current
|
|
73
|
+
|
|
74
|
+
# Input moderation check (before LLM call)
|
|
75
|
+
if self.class.moderation_enabled? && should_moderate?(:input)
|
|
76
|
+
input_text = build_moderation_input
|
|
77
|
+
moderate_input(input_text)
|
|
78
|
+
|
|
79
|
+
if moderation_blocked?
|
|
80
|
+
context.output = build_moderation_blocked_result(:input)
|
|
81
|
+
return
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Execute the LLM call via parent
|
|
86
|
+
client = build_client
|
|
87
|
+
response = execute_llm_call(client, context)
|
|
88
|
+
capture_response(response, context)
|
|
89
|
+
processed_content = process_response(response)
|
|
90
|
+
|
|
91
|
+
# Output moderation check (after LLM call)
|
|
92
|
+
if self.class.moderation_enabled? && should_moderate?(:output)
|
|
93
|
+
output_text = processed_content.is_a?(String) ? processed_content : processed_content.to_s
|
|
94
|
+
moderate_output(output_text)
|
|
95
|
+
|
|
96
|
+
if moderation_blocked?
|
|
97
|
+
context.output = build_moderation_blocked_result(:output)
|
|
98
|
+
return
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
context.output = build_result(processed_content, response, context)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# Returns the resolved tenant ID for tracking
|
|
106
|
+
#
|
|
107
|
+
# @return [String, nil] The tenant identifier
|
|
108
|
+
def resolved_tenant_id
|
|
109
|
+
tenant = resolve_tenant
|
|
110
|
+
return nil unless tenant
|
|
111
|
+
|
|
112
|
+
tenant.is_a?(Hash) ? tenant[:id]&.to_s : nil
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
private
|
|
116
|
+
|
|
117
|
+
# Check if execution model is available for moderation tracking
|
|
118
|
+
#
|
|
119
|
+
# @return [Boolean] true if Execution model can be used
|
|
120
|
+
def execution_model_available?
|
|
121
|
+
return @execution_model_available if defined?(@execution_model_available)
|
|
122
|
+
|
|
123
|
+
@execution_model_available = begin
|
|
124
|
+
RubyLLM::Agents::Execution.table_exists?
|
|
125
|
+
rescue StandardError
|
|
126
|
+
false
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# Load moderation modules after class is defined (they reopen the class)
|
|
134
|
+
require_relative "base/moderation_dsl"
|
|
135
|
+
require_relative "base/moderation_execution"
|