ruby_llm-agents 1.3.3 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (192) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +101 -334
  3. data/app/controllers/concerns/ruby_llm/agents/sortable.rb +0 -1
  4. data/app/controllers/ruby_llm/agents/agents_controller.rb +5 -56
  5. data/app/controllers/ruby_llm/agents/dashboard_controller.rb +22 -106
  6. data/app/controllers/ruby_llm/agents/executions_controller.rb +4 -114
  7. data/app/controllers/ruby_llm/agents/tenants_controller.rb +30 -2
  8. data/app/helpers/ruby_llm/agents/application_helper.rb +19 -53
  9. data/app/models/ruby_llm/agents/execution/analytics.rb +13 -54
  10. data/app/models/ruby_llm/agents/execution/scopes.rb +61 -14
  11. data/app/models/ruby_llm/agents/execution.rb +46 -10
  12. data/app/models/ruby_llm/agents/execution_detail.rb +18 -0
  13. data/app/models/ruby_llm/agents/tenant/budgetable.rb +132 -24
  14. data/app/models/ruby_llm/agents/tenant/incrementable.rb +117 -0
  15. data/app/models/ruby_llm/agents/tenant/resettable.rb +128 -0
  16. data/app/models/ruby_llm/agents/tenant/trackable.rb +46 -12
  17. data/app/models/ruby_llm/agents/tenant.rb +2 -3
  18. data/app/models/ruby_llm/agents/tenant_budget.rb +6 -3
  19. data/app/services/ruby_llm/agents/agent_registry.rb +6 -112
  20. data/app/views/layouts/ruby_llm/agents/application.html.erb +87 -252
  21. data/app/views/ruby_llm/agents/agents/_config_agent.html.erb +71 -218
  22. data/app/views/ruby_llm/agents/agents/_config_embedder.html.erb +20 -63
  23. data/app/views/ruby_llm/agents/agents/_config_image_generator.html.erb +44 -131
  24. data/app/views/ruby_llm/agents/agents/_config_moderator.html.erb +16 -57
  25. data/app/views/ruby_llm/agents/agents/_config_speaker.html.erb +39 -104
  26. data/app/views/ruby_llm/agents/agents/_config_transcriber.html.erb +29 -82
  27. data/app/views/ruby_llm/agents/agents/_empty_state.html.erb +4 -14
  28. data/app/views/ruby_llm/agents/agents/index.html.erb +105 -274
  29. data/app/views/ruby_llm/agents/agents/show.html.erb +248 -378
  30. data/app/views/ruby_llm/agents/dashboard/_action_center.html.erb +29 -52
  31. data/app/views/ruby_llm/agents/dashboard/_tenant_budget.html.erb +73 -99
  32. data/app/views/ruby_llm/agents/dashboard/index.html.erb +228 -433
  33. data/app/views/ruby_llm/agents/executions/_execution.html.erb +1 -1
  34. data/app/views/ruby_llm/agents/executions/_filters.html.erb +4 -25
  35. data/app/views/ruby_llm/agents/executions/_list.html.erb +111 -152
  36. data/app/views/ruby_llm/agents/executions/index.html.erb +5 -7
  37. data/app/views/ruby_llm/agents/executions/show.html.erb +528 -989
  38. data/app/views/ruby_llm/agents/shared/_agent_type_badge.html.erb +5 -21
  39. data/app/views/ruby_llm/agents/shared/_executions_table.html.erb +70 -191
  40. data/app/views/ruby_llm/agents/shared/_filter_dropdown.html.erb +16 -44
  41. data/app/views/ruby_llm/agents/shared/_select_dropdown.html.erb +12 -41
  42. data/app/views/ruby_llm/agents/shared/_status_badge.html.erb +11 -65
  43. data/app/views/ruby_llm/agents/shared/_tenant_filter.html.erb +6 -5
  44. data/app/views/ruby_llm/agents/system_config/show.html.erb +240 -351
  45. data/app/views/ruby_llm/agents/tenants/_form.html.erb +67 -77
  46. data/app/views/ruby_llm/agents/tenants/edit.html.erb +7 -9
  47. data/app/views/ruby_llm/agents/tenants/index.html.erb +100 -122
  48. data/app/views/ruby_llm/agents/tenants/show.html.erb +146 -336
  49. data/config/routes.rb +0 -13
  50. data/lib/generators/ruby_llm_agents/install_generator.rb +9 -14
  51. data/lib/generators/ruby_llm_agents/migrate_structure_generator.rb +2 -12
  52. data/lib/generators/ruby_llm_agents/restructure_generator.rb +0 -2
  53. data/lib/generators/ruby_llm_agents/templates/add_usage_counters_to_tenants_migration.rb.tt +37 -0
  54. data/lib/generators/ruby_llm_agents/templates/agent.rb.tt +1 -2
  55. data/lib/generators/ruby_llm_agents/templates/application_agent.rb.tt +1 -1
  56. data/lib/generators/ruby_llm_agents/templates/application_image_pipeline.rb.tt +0 -1
  57. data/lib/generators/ruby_llm_agents/templates/create_execution_details_migration.rb.tt +27 -0
  58. data/lib/generators/ruby_llm_agents/templates/create_tenants_migration.rb.tt +25 -0
  59. data/lib/generators/ruby_llm_agents/templates/image_pipeline.rb.tt +0 -1
  60. data/lib/generators/ruby_llm_agents/templates/initializer.rb.tt +9 -12
  61. data/lib/generators/ruby_llm_agents/templates/migration.rb.tt +40 -71
  62. data/lib/generators/ruby_llm_agents/templates/remove_agent_version_migration.rb.tt +13 -0
  63. data/lib/generators/ruby_llm_agents/templates/remove_workflow_columns_migration.rb.tt +19 -0
  64. data/lib/generators/ruby_llm_agents/templates/skills/AGENTS.md.tt +2 -4
  65. data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_PIPELINES.md.tt +0 -1
  66. data/lib/generators/ruby_llm_agents/templates/split_execution_details_migration.rb.tt +232 -0
  67. data/lib/generators/ruby_llm_agents/upgrade_generator.rb +58 -262
  68. data/lib/ruby_llm/agents/audio/speaker.rb +0 -1
  69. data/lib/ruby_llm/agents/audio/transcriber.rb +0 -1
  70. data/lib/ruby_llm/agents/base_agent.rb +52 -6
  71. data/lib/ruby_llm/agents/core/base/callbacks.rb +142 -0
  72. data/lib/ruby_llm/agents/core/base.rb +23 -55
  73. data/lib/ruby_llm/agents/core/configuration.rb +58 -117
  74. data/lib/ruby_llm/agents/core/errors.rb +0 -58
  75. data/lib/ruby_llm/agents/core/instrumentation.rb +157 -110
  76. data/lib/ruby_llm/agents/core/llm_tenant.rb +8 -7
  77. data/lib/ruby_llm/agents/core/version.rb +1 -1
  78. data/lib/ruby_llm/agents/dsl/base.rb +157 -17
  79. data/lib/ruby_llm/agents/dsl/caching.rb +33 -2
  80. data/lib/ruby_llm/agents/dsl/reliability.rb +148 -0
  81. data/lib/ruby_llm/agents/dsl.rb +1 -2
  82. data/lib/ruby_llm/agents/image/analyzer/execution.rb +1 -2
  83. data/lib/ruby_llm/agents/image/background_remover/execution.rb +1 -2
  84. data/lib/ruby_llm/agents/image/concerns/image_operation_dsl.rb +1 -13
  85. data/lib/ruby_llm/agents/image/concerns/image_operation_execution.rb +2 -2
  86. data/lib/ruby_llm/agents/image/editor/dsl.rb +0 -14
  87. data/lib/ruby_llm/agents/image/editor/execution.rb +1 -10
  88. data/lib/ruby_llm/agents/image/editor.rb +0 -1
  89. data/lib/ruby_llm/agents/image/generator.rb +0 -21
  90. data/lib/ruby_llm/agents/image/pipeline/dsl.rb +0 -13
  91. data/lib/ruby_llm/agents/image/pipeline/execution.rb +0 -1
  92. data/lib/ruby_llm/agents/image/transformer/dsl.rb +0 -13
  93. data/lib/ruby_llm/agents/image/transformer/execution.rb +1 -10
  94. data/lib/ruby_llm/agents/image/transformer.rb +0 -1
  95. data/lib/ruby_llm/agents/image/upscaler/execution.rb +1 -2
  96. data/lib/ruby_llm/agents/image/variator/execution.rb +1 -2
  97. data/lib/ruby_llm/agents/infrastructure/alert_manager.rb +78 -173
  98. data/lib/ruby_llm/agents/infrastructure/attempt_tracker.rb +1 -0
  99. data/lib/ruby_llm/agents/infrastructure/budget/budget_query.rb +66 -2
  100. data/lib/ruby_llm/agents/infrastructure/budget/spend_recorder.rb +0 -12
  101. data/lib/ruby_llm/agents/infrastructure/circuit_breaker.rb +10 -13
  102. data/lib/ruby_llm/agents/infrastructure/reliability.rb +37 -2
  103. data/lib/ruby_llm/agents/pipeline/context.rb +0 -1
  104. data/lib/ruby_llm/agents/pipeline/middleware/budget.rb +28 -4
  105. data/lib/ruby_llm/agents/pipeline/middleware/cache.rb +3 -10
  106. data/lib/ruby_llm/agents/pipeline/middleware/instrumentation.rb +88 -55
  107. data/lib/ruby_llm/agents/pipeline/middleware/tenant.rb +5 -41
  108. data/lib/ruby_llm/agents/rails/engine.rb +6 -6
  109. data/lib/ruby_llm/agents/results/base.rb +1 -49
  110. data/lib/ruby_llm/agents/text/embedder.rb +0 -1
  111. data/lib/ruby_llm/agents.rb +1 -9
  112. data/lib/tasks/ruby_llm_agents.rake +34 -0
  113. metadata +12 -81
  114. data/app/controllers/ruby_llm/agents/api_configurations_controller.rb +0 -214
  115. data/app/controllers/ruby_llm/agents/workflows_controller.rb +0 -544
  116. data/app/mailers/ruby_llm/agents/alert_mailer.rb +0 -84
  117. data/app/mailers/ruby_llm/agents/application_mailer.rb +0 -28
  118. data/app/models/ruby_llm/agents/api_configuration.rb +0 -386
  119. data/app/models/ruby_llm/agents/execution/workflow.rb +0 -170
  120. data/app/models/ruby_llm/agents/tenant/configurable.rb +0 -135
  121. data/app/views/ruby_llm/agents/agents/_agent.html.erb +0 -98
  122. data/app/views/ruby_llm/agents/agents/_version_comparison.html.erb +0 -186
  123. data/app/views/ruby_llm/agents/agents/_workflow.html.erb +0 -126
  124. data/app/views/ruby_llm/agents/alert_mailer/alert_notification.html.erb +0 -107
  125. data/app/views/ruby_llm/agents/alert_mailer/alert_notification.text.erb +0 -18
  126. data/app/views/ruby_llm/agents/api_configurations/_api_key_field.html.erb +0 -34
  127. data/app/views/ruby_llm/agents/api_configurations/_form.html.erb +0 -288
  128. data/app/views/ruby_llm/agents/api_configurations/edit.html.erb +0 -95
  129. data/app/views/ruby_llm/agents/api_configurations/edit_tenant.html.erb +0 -97
  130. data/app/views/ruby_llm/agents/api_configurations/show.html.erb +0 -214
  131. data/app/views/ruby_llm/agents/api_configurations/tenant.html.erb +0 -179
  132. data/app/views/ruby_llm/agents/dashboard/_agent_comparison.html.erb +0 -73
  133. data/app/views/ruby_llm/agents/dashboard/_alerts_feed.html.erb +0 -62
  134. data/app/views/ruby_llm/agents/dashboard/_breaker_strip.html.erb +0 -47
  135. data/app/views/ruby_llm/agents/dashboard/_budgets_bar.html.erb +0 -75
  136. data/app/views/ruby_llm/agents/dashboard/_model_comparison.html.erb +0 -56
  137. data/app/views/ruby_llm/agents/dashboard/_model_cost_breakdown.html.erb +0 -115
  138. data/app/views/ruby_llm/agents/dashboard/_now_strip.html.erb +0 -59
  139. data/app/views/ruby_llm/agents/dashboard/_top_errors.html.erb +0 -60
  140. data/app/views/ruby_llm/agents/executions/_workflow_summary.html.erb +0 -86
  141. data/app/views/ruby_llm/agents/executions/dry_run.html.erb +0 -149
  142. data/app/views/ruby_llm/agents/shared/_breadcrumbs.html.erb +0 -48
  143. data/app/views/ruby_llm/agents/shared/_nav_link.html.erb +0 -27
  144. data/app/views/ruby_llm/agents/shared/_stat_card.html.erb +0 -14
  145. data/app/views/ruby_llm/agents/shared/_workflow_type_badge.html.erb +0 -35
  146. data/app/views/ruby_llm/agents/workflows/_empty_state.html.erb +0 -22
  147. data/app/views/ruby_llm/agents/workflows/_step_performance.html.erb +0 -228
  148. data/app/views/ruby_llm/agents/workflows/_structure_dsl.html.erb +0 -539
  149. data/app/views/ruby_llm/agents/workflows/_structure_parallel.html.erb +0 -76
  150. data/app/views/ruby_llm/agents/workflows/_structure_pipeline.html.erb +0 -74
  151. data/app/views/ruby_llm/agents/workflows/_structure_router.html.erb +0 -108
  152. data/app/views/ruby_llm/agents/workflows/_workflow_diagram.html.erb +0 -920
  153. data/app/views/ruby_llm/agents/workflows/index.html.erb +0 -179
  154. data/app/views/ruby_llm/agents/workflows/show.html.erb +0 -467
  155. data/lib/generators/ruby_llm_agents/api_configuration_generator.rb +0 -100
  156. data/lib/generators/ruby_llm_agents/templates/add_workflow_migration.rb.tt +0 -38
  157. data/lib/generators/ruby_llm_agents/templates/application_workflow.rb.tt +0 -48
  158. data/lib/generators/ruby_llm_agents/templates/create_api_configurations_migration.rb.tt +0 -90
  159. data/lib/generators/ruby_llm_agents/templates/skills/WORKFLOWS.md.tt +0 -551
  160. data/lib/ruby_llm/agents/core/base/moderation_dsl.rb +0 -181
  161. data/lib/ruby_llm/agents/core/base/moderation_execution.rb +0 -274
  162. data/lib/ruby_llm/agents/core/resolved_config.rb +0 -348
  163. data/lib/ruby_llm/agents/image/generator/content_policy.rb +0 -95
  164. data/lib/ruby_llm/agents/infrastructure/redactor.rb +0 -130
  165. data/lib/ruby_llm/agents/results/moderation_result.rb +0 -158
  166. data/lib/ruby_llm/agents/text/moderator.rb +0 -237
  167. data/lib/ruby_llm/agents/workflow/approval.rb +0 -205
  168. data/lib/ruby_llm/agents/workflow/approval_store.rb +0 -179
  169. data/lib/ruby_llm/agents/workflow/async.rb +0 -220
  170. data/lib/ruby_llm/agents/workflow/async_executor.rb +0 -156
  171. data/lib/ruby_llm/agents/workflow/dsl/executor.rb +0 -467
  172. data/lib/ruby_llm/agents/workflow/dsl/input_schema.rb +0 -244
  173. data/lib/ruby_llm/agents/workflow/dsl/iteration_executor.rb +0 -289
  174. data/lib/ruby_llm/agents/workflow/dsl/parallel_group.rb +0 -107
  175. data/lib/ruby_llm/agents/workflow/dsl/route_builder.rb +0 -150
  176. data/lib/ruby_llm/agents/workflow/dsl/schedule_helpers.rb +0 -187
  177. data/lib/ruby_llm/agents/workflow/dsl/step_config.rb +0 -352
  178. data/lib/ruby_llm/agents/workflow/dsl/step_executor.rb +0 -415
  179. data/lib/ruby_llm/agents/workflow/dsl/wait_config.rb +0 -257
  180. data/lib/ruby_llm/agents/workflow/dsl/wait_executor.rb +0 -317
  181. data/lib/ruby_llm/agents/workflow/dsl.rb +0 -576
  182. data/lib/ruby_llm/agents/workflow/instrumentation.rb +0 -249
  183. data/lib/ruby_llm/agents/workflow/notifiers/base.rb +0 -117
  184. data/lib/ruby_llm/agents/workflow/notifiers/email.rb +0 -117
  185. data/lib/ruby_llm/agents/workflow/notifiers/slack.rb +0 -180
  186. data/lib/ruby_llm/agents/workflow/notifiers/webhook.rb +0 -121
  187. data/lib/ruby_llm/agents/workflow/notifiers.rb +0 -70
  188. data/lib/ruby_llm/agents/workflow/orchestrator.rb +0 -416
  189. data/lib/ruby_llm/agents/workflow/result.rb +0 -592
  190. data/lib/ruby_llm/agents/workflow/thread_pool.rb +0 -185
  191. data/lib/ruby_llm/agents/workflow/throttle_manager.rb +0 -206
  192. data/lib/ruby_llm/agents/workflow/wait_result.rb +0 -213
@@ -1,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