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
|
@@ -15,7 +15,6 @@ module RubyLLM
|
|
|
15
15
|
# @example Creating an agent
|
|
16
16
|
# class SearchAgent < RubyLLM::Agents::BaseAgent
|
|
17
17
|
# model "gpt-4o"
|
|
18
|
-
# version "1.0"
|
|
19
18
|
# description "Searches for relevant documents"
|
|
20
19
|
# timeout 30
|
|
21
20
|
#
|
|
@@ -246,24 +245,38 @@ module RubyLLM
|
|
|
246
245
|
|
|
247
246
|
# User prompt to send to the LLM
|
|
248
247
|
#
|
|
249
|
-
#
|
|
248
|
+
# If a class-level `prompt` DSL is defined (string template or block),
|
|
249
|
+
# it will be used. Otherwise, subclasses must implement this method.
|
|
250
|
+
#
|
|
250
251
|
# @return [String] The user prompt
|
|
251
252
|
def user_prompt
|
|
252
|
-
|
|
253
|
+
prompt_config = self.class.prompt_config
|
|
254
|
+
return resolve_prompt_from_config(prompt_config) if prompt_config
|
|
255
|
+
|
|
256
|
+
raise NotImplementedError, "#{self.class} must implement #user_prompt or use the prompt DSL"
|
|
253
257
|
end
|
|
254
258
|
|
|
255
259
|
# System prompt for LLM instructions
|
|
256
260
|
#
|
|
261
|
+
# If a class-level `system` DSL is defined, it will be used.
|
|
262
|
+
# Otherwise returns nil.
|
|
263
|
+
#
|
|
257
264
|
# @return [String, nil] System instructions, or nil for none
|
|
258
265
|
def system_prompt
|
|
266
|
+
system_config = self.class.system_config
|
|
267
|
+
return resolve_prompt_from_config(system_config) if system_config
|
|
268
|
+
|
|
259
269
|
nil
|
|
260
270
|
end
|
|
261
271
|
|
|
262
272
|
# Response schema for structured output
|
|
263
273
|
#
|
|
274
|
+
# Delegates to the class-level schema DSL by default.
|
|
275
|
+
# Override in subclass instances to customize per-instance.
|
|
276
|
+
#
|
|
264
277
|
# @return [RubyLLM::Schema, nil] Schema definition, or nil for free-form
|
|
265
278
|
def schema
|
|
266
|
-
|
|
279
|
+
self.class.schema
|
|
267
280
|
end
|
|
268
281
|
|
|
269
282
|
# Conversation history for multi-turn conversations
|
|
@@ -288,9 +301,12 @@ module RubyLLM
|
|
|
288
301
|
|
|
289
302
|
# Generates the cache key for this agent invocation
|
|
290
303
|
#
|
|
291
|
-
#
|
|
304
|
+
# Cache keys are content-based, using a hash of the prompts and parameters.
|
|
305
|
+
# This automatically invalidates caches when prompts change.
|
|
306
|
+
#
|
|
307
|
+
# @return [String] Cache key in format "ruby_llm_agent/ClassName/hash"
|
|
292
308
|
def agent_cache_key
|
|
293
|
-
["ruby_llm_agent", self.class.name,
|
|
309
|
+
["ruby_llm_agent", self.class.name, cache_key_hash].join("/")
|
|
294
310
|
end
|
|
295
311
|
|
|
296
312
|
# Generates a hash of the cache key data
|
|
@@ -462,6 +478,36 @@ module RubyLLM
|
|
|
462
478
|
end
|
|
463
479
|
end
|
|
464
480
|
|
|
481
|
+
# Resolves a prompt from DSL configuration (template string or block)
|
|
482
|
+
#
|
|
483
|
+
# For string templates, interpolates {placeholder} with parameter values.
|
|
484
|
+
# For blocks, evaluates in the instance context.
|
|
485
|
+
#
|
|
486
|
+
# @param config [String, Proc] The prompt configuration
|
|
487
|
+
# @return [String] The resolved prompt
|
|
488
|
+
def resolve_prompt_from_config(config)
|
|
489
|
+
case config
|
|
490
|
+
when String
|
|
491
|
+
interpolate_template(config)
|
|
492
|
+
when Proc
|
|
493
|
+
instance_eval(&config)
|
|
494
|
+
else
|
|
495
|
+
config.to_s
|
|
496
|
+
end
|
|
497
|
+
end
|
|
498
|
+
|
|
499
|
+
# Interpolates {placeholder} patterns in a template string
|
|
500
|
+
#
|
|
501
|
+
# @param template [String] Template with {placeholder} syntax
|
|
502
|
+
# @return [String] Interpolated string
|
|
503
|
+
def interpolate_template(template)
|
|
504
|
+
template.gsub(/\{(\w+)\}/) do
|
|
505
|
+
param_name = ::Regexp.last_match(1).to_sym
|
|
506
|
+
value = send(param_name) if respond_to?(param_name)
|
|
507
|
+
value.to_s
|
|
508
|
+
end
|
|
509
|
+
end
|
|
510
|
+
|
|
465
511
|
# Execute the core LLM call
|
|
466
512
|
#
|
|
467
513
|
# This is called by the Pipeline::Executor after all middleware
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
module Agents
|
|
5
|
+
# DSL and execution support for before_call/after_call hooks
|
|
6
|
+
#
|
|
7
|
+
# Provides callbacks that run before and after the LLM call,
|
|
8
|
+
# allowing custom preprocessing (redaction, moderation, validation)
|
|
9
|
+
# and postprocessing (logging, transformation) logic.
|
|
10
|
+
#
|
|
11
|
+
# @example Using callbacks
|
|
12
|
+
# class MyAgent < ApplicationAgent
|
|
13
|
+
# before_call :sanitize_input
|
|
14
|
+
# after_call :log_response
|
|
15
|
+
#
|
|
16
|
+
# # Or with blocks
|
|
17
|
+
# before_call { |context| context.params[:timestamp] = Time.current }
|
|
18
|
+
# after_call { |context, response| notify(response) }
|
|
19
|
+
#
|
|
20
|
+
# private
|
|
21
|
+
#
|
|
22
|
+
# def sanitize_input(context)
|
|
23
|
+
# # Mutate context as needed
|
|
24
|
+
# # Raise to block execution
|
|
25
|
+
# end
|
|
26
|
+
#
|
|
27
|
+
# def log_response(context, response)
|
|
28
|
+
# Rails.logger.info("Response: #{response}")
|
|
29
|
+
# end
|
|
30
|
+
# end
|
|
31
|
+
#
|
|
32
|
+
module CallbacksDSL
|
|
33
|
+
# Add a callback to run before the LLM call
|
|
34
|
+
#
|
|
35
|
+
# Callbacks receive the pipeline context and can:
|
|
36
|
+
# - Mutate the context (params, prompts, etc.)
|
|
37
|
+
# - Raise an exception to block execution
|
|
38
|
+
# - Return value is ignored
|
|
39
|
+
#
|
|
40
|
+
# @param method_name [Symbol, nil] Instance method to call
|
|
41
|
+
# @yield [context] Block to execute
|
|
42
|
+
# @yieldparam context [Pipeline::Context] The execution context
|
|
43
|
+
# @return [void]
|
|
44
|
+
#
|
|
45
|
+
# @example With method name
|
|
46
|
+
# before_call :validate_input
|
|
47
|
+
#
|
|
48
|
+
# @example With block
|
|
49
|
+
# before_call { |context| context.params[:sanitized] = true }
|
|
50
|
+
#
|
|
51
|
+
def before_call(method_name = nil, &block)
|
|
52
|
+
@callbacks ||= { before: [], after: [] }
|
|
53
|
+
@callbacks[:before] << (block || method_name)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Add a callback to run after the LLM call
|
|
57
|
+
#
|
|
58
|
+
# Callbacks receive the pipeline context and the response.
|
|
59
|
+
# Return value is ignored.
|
|
60
|
+
#
|
|
61
|
+
# @param method_name [Symbol, nil] Instance method to call
|
|
62
|
+
# @yield [context, response] Block to execute
|
|
63
|
+
# @yieldparam context [Pipeline::Context] The execution context
|
|
64
|
+
# @yieldparam response [Object] The LLM response
|
|
65
|
+
# @return [void]
|
|
66
|
+
#
|
|
67
|
+
# @example With method name
|
|
68
|
+
# after_call :log_response
|
|
69
|
+
#
|
|
70
|
+
# @example With block
|
|
71
|
+
# after_call { |context, response| notify_completion(response) }
|
|
72
|
+
#
|
|
73
|
+
def after_call(method_name = nil, &block)
|
|
74
|
+
@callbacks ||= { before: [], after: [] }
|
|
75
|
+
@callbacks[:after] << (block || method_name)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Simplified alias for before_call (block-only)
|
|
79
|
+
#
|
|
80
|
+
# This is the preferred method in the simplified DSL.
|
|
81
|
+
#
|
|
82
|
+
# @yield [context] Block to execute before the LLM call
|
|
83
|
+
# @yieldparam context [Pipeline::Context] The execution context
|
|
84
|
+
# @return [void]
|
|
85
|
+
#
|
|
86
|
+
# @example
|
|
87
|
+
# before { |ctx| ctx.params[:timestamp] = Time.current }
|
|
88
|
+
# before { |ctx| validate_input!(ctx.params[:query]) }
|
|
89
|
+
#
|
|
90
|
+
def before(&block)
|
|
91
|
+
before_call(&block)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Simplified alias for after_call (block-only)
|
|
95
|
+
#
|
|
96
|
+
# This is the preferred method in the simplified DSL.
|
|
97
|
+
#
|
|
98
|
+
# @yield [context, response] Block to execute after the LLM call
|
|
99
|
+
# @yieldparam context [Pipeline::Context] The execution context
|
|
100
|
+
# @yieldparam response [Object] The LLM response
|
|
101
|
+
# @return [void]
|
|
102
|
+
#
|
|
103
|
+
# @example
|
|
104
|
+
# after { |ctx, result| Rails.logger.info("Completed: #{result}") }
|
|
105
|
+
# after { |ctx, result| notify_slack(result) if result.confidence < 0.5 }
|
|
106
|
+
#
|
|
107
|
+
def after(&block)
|
|
108
|
+
after_call(&block)
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# Get all registered callbacks
|
|
112
|
+
#
|
|
113
|
+
# @return [Hash] Hash with :before and :after arrays
|
|
114
|
+
def callbacks
|
|
115
|
+
@callbacks ||= { before: [], after: [] }
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# Instance methods for running callbacks
|
|
120
|
+
module CallbacksExecution
|
|
121
|
+
private
|
|
122
|
+
|
|
123
|
+
# Run callbacks of the specified type
|
|
124
|
+
#
|
|
125
|
+
# @param type [Symbol] :before or :after
|
|
126
|
+
# @param args [Array] Arguments to pass to callbacks
|
|
127
|
+
# @return [void]
|
|
128
|
+
def run_callbacks(type, *args)
|
|
129
|
+
callbacks = self.class.callbacks[type] || []
|
|
130
|
+
|
|
131
|
+
callbacks.each do |callback|
|
|
132
|
+
case callback
|
|
133
|
+
when Symbol
|
|
134
|
+
send(callback, *args)
|
|
135
|
+
when Proc
|
|
136
|
+
instance_exec(*args, &callback)
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
end
|
|
@@ -1,20 +1,18 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require_relative "base/
|
|
4
|
-
require_relative "base/moderation_execution"
|
|
3
|
+
require_relative "base/callbacks"
|
|
5
4
|
|
|
6
5
|
module RubyLLM
|
|
7
6
|
module Agents
|
|
8
7
|
# Base class for LLM-powered conversational agents
|
|
9
8
|
#
|
|
10
9
|
# Inherits from BaseAgent to use the middleware pipeline architecture
|
|
11
|
-
# while adding
|
|
10
|
+
# while adding callback hooks for custom preprocessing and postprocessing.
|
|
12
11
|
#
|
|
13
12
|
# @example Creating an agent
|
|
14
13
|
# class SearchAgent < ApplicationAgent
|
|
15
14
|
# model "gpt-4o"
|
|
16
15
|
# temperature 0.0
|
|
17
|
-
# version "1.0"
|
|
18
16
|
# timeout 30
|
|
19
17
|
# cache_for 1.hour
|
|
20
18
|
#
|
|
@@ -30,14 +28,19 @@ module RubyLLM
|
|
|
30
28
|
# end
|
|
31
29
|
# end
|
|
32
30
|
#
|
|
33
|
-
# @example With
|
|
31
|
+
# @example With callbacks
|
|
34
32
|
# class SafeAgent < ApplicationAgent
|
|
35
|
-
#
|
|
36
|
-
#
|
|
37
|
-
# moderation :both
|
|
33
|
+
# before_call :redact_pii
|
|
34
|
+
# after_call :log_response
|
|
38
35
|
#
|
|
39
|
-
#
|
|
40
|
-
#
|
|
36
|
+
# private
|
|
37
|
+
#
|
|
38
|
+
# def redact_pii(context)
|
|
39
|
+
# # Custom redaction logic
|
|
40
|
+
# end
|
|
41
|
+
#
|
|
42
|
+
# def log_response(context, response)
|
|
43
|
+
# Rails.logger.info("Response received")
|
|
41
44
|
# end
|
|
42
45
|
# end
|
|
43
46
|
#
|
|
@@ -49,8 +52,8 @@ module RubyLLM
|
|
|
49
52
|
# @see RubyLLM::Agents::BaseAgent
|
|
50
53
|
# @api public
|
|
51
54
|
class Base < BaseAgent
|
|
52
|
-
extend
|
|
53
|
-
include
|
|
55
|
+
extend CallbacksDSL
|
|
56
|
+
include CallbacksExecution
|
|
54
57
|
|
|
55
58
|
class << self
|
|
56
59
|
# Returns the agent type for conversation agents
|
|
@@ -61,43 +64,27 @@ module RubyLLM
|
|
|
61
64
|
end
|
|
62
65
|
end
|
|
63
66
|
|
|
64
|
-
# Execute the core LLM call with
|
|
67
|
+
# Execute the core LLM call with callback support
|
|
65
68
|
#
|
|
66
|
-
# This extends BaseAgent's execute method to add
|
|
67
|
-
#
|
|
69
|
+
# This extends BaseAgent's execute method to add before/after
|
|
70
|
+
# callback hooks for custom preprocessing and postprocessing.
|
|
68
71
|
#
|
|
69
72
|
# @param context [Pipeline::Context] The execution context
|
|
70
73
|
# @return [void] Sets context.output with the result
|
|
71
74
|
def execute(context)
|
|
72
75
|
@execution_started_at = context.started_at || Time.current
|
|
73
76
|
|
|
74
|
-
#
|
|
75
|
-
|
|
76
|
-
input_text = build_moderation_input
|
|
77
|
-
moderate_input(input_text)
|
|
77
|
+
# Run before_call callbacks
|
|
78
|
+
run_callbacks(:before, context)
|
|
78
79
|
|
|
79
|
-
|
|
80
|
-
context.output = build_moderation_blocked_result(:input)
|
|
81
|
-
return
|
|
82
|
-
end
|
|
83
|
-
end
|
|
84
|
-
|
|
85
|
-
# Execute the LLM call via parent
|
|
80
|
+
# Execute the LLM call
|
|
86
81
|
client = build_client(context)
|
|
87
82
|
response = execute_llm_call(client, context)
|
|
88
83
|
capture_response(response, context)
|
|
89
84
|
processed_content = process_response(response)
|
|
90
85
|
|
|
91
|
-
#
|
|
92
|
-
|
|
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
|
|
86
|
+
# Run after_call callbacks
|
|
87
|
+
run_callbacks(:after, context, response)
|
|
101
88
|
|
|
102
89
|
context.output = build_result(processed_content, response, context)
|
|
103
90
|
end
|
|
@@ -111,25 +98,6 @@ module RubyLLM
|
|
|
111
98
|
|
|
112
99
|
tenant.is_a?(Hash) ? tenant[:id]&.to_s : nil
|
|
113
100
|
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
101
|
end
|
|
130
102
|
end
|
|
131
103
|
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"
|
|
@@ -167,15 +167,18 @@ module RubyLLM
|
|
|
167
167
|
# enforcement: :soft
|
|
168
168
|
# }
|
|
169
169
|
|
|
170
|
-
# @!attribute [rw]
|
|
171
|
-
# Alert
|
|
172
|
-
#
|
|
170
|
+
# @!attribute [rw] on_alert
|
|
171
|
+
# Alert handler proc called when governance events occur.
|
|
172
|
+
# Receives event name and payload hash. Filter events in your proc as needed.
|
|
173
|
+
# @return [Proc, nil] Alert handler or nil to disable (default: nil)
|
|
173
174
|
# @example
|
|
174
|
-
# config.
|
|
175
|
-
#
|
|
176
|
-
#
|
|
177
|
-
#
|
|
178
|
-
#
|
|
175
|
+
# config.on_alert = ->(event, payload) {
|
|
176
|
+
# case event
|
|
177
|
+
# when :budget_hard_cap
|
|
178
|
+
# Slack::Notifier.new(ENV["SLACK_WEBHOOK"]).ping("Budget exceeded")
|
|
179
|
+
# when :breaker_open
|
|
180
|
+
# PagerDuty.trigger(payload)
|
|
181
|
+
# end
|
|
179
182
|
# }
|
|
180
183
|
|
|
181
184
|
# @!attribute [rw] persist_prompts
|
|
@@ -188,17 +191,6 @@ module RubyLLM
|
|
|
188
191
|
# Set to false to reduce storage or for privacy compliance.
|
|
189
192
|
# @return [Boolean] Enable response persistence (default: true)
|
|
190
193
|
|
|
191
|
-
# @!attribute [rw] redaction
|
|
192
|
-
# Redaction configuration for PII and sensitive data.
|
|
193
|
-
# @return [Hash, nil] Redaction config with :fields, :patterns, :placeholder, :max_value_length keys
|
|
194
|
-
# @example
|
|
195
|
-
# config.redaction = {
|
|
196
|
-
# fields: %w[password api_key email ssn],
|
|
197
|
-
# patterns: [/\b\d{3}-\d{2}-\d{4}\b/],
|
|
198
|
-
# placeholder: "[REDACTED]",
|
|
199
|
-
# max_value_length: 5000
|
|
200
|
-
# }
|
|
201
|
-
|
|
202
194
|
# @!attribute [rw] multi_tenancy_enabled
|
|
203
195
|
# Whether multi-tenancy features are enabled.
|
|
204
196
|
# When false, the gem behaves exactly as before (backward compatible).
|
|
@@ -274,35 +266,6 @@ module RubyLLM
|
|
|
274
266
|
# @example
|
|
275
267
|
# config.track_embeddings = false
|
|
276
268
|
|
|
277
|
-
# @!attribute [rw] default_moderation_model
|
|
278
|
-
# The default moderation model identifier for all agents.
|
|
279
|
-
# Can be overridden per-agent using the `moderation` DSL method.
|
|
280
|
-
# @return [String] Model identifier (default: "omni-moderation-latest")
|
|
281
|
-
# @example
|
|
282
|
-
# config.default_moderation_model = "text-moderation-007"
|
|
283
|
-
|
|
284
|
-
# @!attribute [rw] default_moderation_threshold
|
|
285
|
-
# The default threshold for moderation scores.
|
|
286
|
-
# Content with scores at or above this threshold will be flagged.
|
|
287
|
-
# Set to nil to use the provider's default flagging.
|
|
288
|
-
# @return [Float, nil] Threshold (0.0-1.0) or nil for provider default (default: nil)
|
|
289
|
-
# @example
|
|
290
|
-
# config.default_moderation_threshold = 0.8
|
|
291
|
-
|
|
292
|
-
# @!attribute [rw] default_moderation_action
|
|
293
|
-
# The default action when content is flagged.
|
|
294
|
-
# Can be overridden per-agent using the `moderation` DSL method.
|
|
295
|
-
# @return [Symbol] Action (:block, :raise, :warn, :log) (default: :block)
|
|
296
|
-
# @example
|
|
297
|
-
# config.default_moderation_action = :raise
|
|
298
|
-
|
|
299
|
-
# @!attribute [rw] track_moderation
|
|
300
|
-
# Whether to track moderation executions in the database.
|
|
301
|
-
# When enabled, moderation operations are logged as executions.
|
|
302
|
-
# @return [Boolean] Enable moderation tracking (default: true)
|
|
303
|
-
# @example
|
|
304
|
-
# config.track_moderation = false
|
|
305
|
-
|
|
306
269
|
# @!attribute [rw] default_transcription_model
|
|
307
270
|
# The default transcription model identifier for all transcribers.
|
|
308
271
|
# Can be overridden per-transcriber using the `model` DSL method.
|
|
@@ -376,6 +339,18 @@ module RubyLLM
|
|
|
376
339
|
# @example
|
|
377
340
|
# config.tool_result_max_length = 5000
|
|
378
341
|
|
|
342
|
+
# @!attribute [rw] redaction
|
|
343
|
+
# Configuration for PII and sensitive data redaction.
|
|
344
|
+
# When set, sensitive data is redacted before storing in execution records.
|
|
345
|
+
# @return [Hash, nil] Redaction config with :fields, :patterns, :placeholder, :max_value_length keys
|
|
346
|
+
# @example
|
|
347
|
+
# config.redaction = {
|
|
348
|
+
# fields: %w[ssn credit_card phone_number email],
|
|
349
|
+
# patterns: [/\b\d{3}-\d{2}-\d{4}\b/],
|
|
350
|
+
# placeholder: "[REDACTED]",
|
|
351
|
+
# max_value_length: 5000
|
|
352
|
+
# }
|
|
353
|
+
|
|
379
354
|
# Attributes without validation (simple accessors)
|
|
380
355
|
attr_accessor :default_model,
|
|
381
356
|
:async_logging,
|
|
@@ -389,10 +364,9 @@ module RubyLLM
|
|
|
389
364
|
:default_streaming,
|
|
390
365
|
:default_tools,
|
|
391
366
|
:default_thinking,
|
|
392
|
-
:
|
|
367
|
+
:on_alert,
|
|
393
368
|
:persist_prompts,
|
|
394
369
|
:persist_responses,
|
|
395
|
-
:redaction,
|
|
396
370
|
:multi_tenancy_enabled,
|
|
397
371
|
:persist_messages_summary,
|
|
398
372
|
:default_retryable_patterns,
|
|
@@ -400,10 +374,6 @@ module RubyLLM
|
|
|
400
374
|
:default_embedding_dimensions,
|
|
401
375
|
:default_embedding_batch_size,
|
|
402
376
|
:track_embeddings,
|
|
403
|
-
:default_moderation_model,
|
|
404
|
-
:default_moderation_threshold,
|
|
405
|
-
:default_moderation_action,
|
|
406
|
-
:track_moderation,
|
|
407
377
|
:default_transcription_model,
|
|
408
378
|
:track_transcriptions,
|
|
409
379
|
:default_tts_provider,
|
|
@@ -437,7 +407,8 @@ module RubyLLM
|
|
|
437
407
|
:default_background_output_format,
|
|
438
408
|
:root_directory,
|
|
439
409
|
:root_namespace,
|
|
440
|
-
:tool_result_max_length
|
|
410
|
+
:tool_result_max_length,
|
|
411
|
+
:redaction
|
|
441
412
|
|
|
442
413
|
# Attributes with validation (readers only, custom setters below)
|
|
443
414
|
attr_reader :default_temperature,
|
|
@@ -634,10 +605,9 @@ module RubyLLM
|
|
|
634
605
|
|
|
635
606
|
# Governance defaults
|
|
636
607
|
@budgets = nil
|
|
637
|
-
@
|
|
608
|
+
@on_alert = nil
|
|
638
609
|
@persist_prompts = true
|
|
639
610
|
@persist_responses = true
|
|
640
|
-
@redaction = nil
|
|
641
611
|
|
|
642
612
|
# Multi-tenancy defaults (disabled for backward compatibility)
|
|
643
613
|
@multi_tenancy_enabled = false
|
|
@@ -654,12 +624,6 @@ module RubyLLM
|
|
|
654
624
|
@default_embedding_batch_size = 100
|
|
655
625
|
@track_embeddings = true
|
|
656
626
|
|
|
657
|
-
# Moderation defaults
|
|
658
|
-
@default_moderation_model = "omni-moderation-latest"
|
|
659
|
-
@default_moderation_threshold = nil
|
|
660
|
-
@default_moderation_action = :block
|
|
661
|
-
@track_moderation = true
|
|
662
|
-
|
|
663
627
|
# Transcription defaults
|
|
664
628
|
@default_transcription_model = "whisper-1"
|
|
665
629
|
@track_transcriptions = true
|
|
@@ -715,6 +679,9 @@ module RubyLLM
|
|
|
715
679
|
|
|
716
680
|
# Tool tracking defaults
|
|
717
681
|
@tool_result_max_length = 10_000
|
|
682
|
+
|
|
683
|
+
# Redaction defaults (disabled by default)
|
|
684
|
+
@redaction = nil
|
|
718
685
|
end
|
|
719
686
|
|
|
720
687
|
# Returns the configured cache store, falling back to Rails.cache
|
|
@@ -747,55 +714,6 @@ module RubyLLM
|
|
|
747
714
|
default_retryable_patterns.values.flatten.uniq
|
|
748
715
|
end
|
|
749
716
|
|
|
750
|
-
# Returns whether alerts are configured
|
|
751
|
-
#
|
|
752
|
-
# @return [Boolean] true if any alert destination is configured
|
|
753
|
-
def alerts_enabled?
|
|
754
|
-
return false unless alerts.is_a?(Hash)
|
|
755
|
-
|
|
756
|
-
alerts[:slack_webhook_url].present? ||
|
|
757
|
-
alerts[:webhook_url].present? ||
|
|
758
|
-
alerts[:custom].present? ||
|
|
759
|
-
alerts[:email_recipients].present?
|
|
760
|
-
end
|
|
761
|
-
|
|
762
|
-
# Returns the list of events to alert on
|
|
763
|
-
#
|
|
764
|
-
# @return [Array<Symbol>] Event names to trigger alerts
|
|
765
|
-
def alert_events
|
|
766
|
-
alerts&.dig(:on_events) || []
|
|
767
|
-
end
|
|
768
|
-
|
|
769
|
-
# Returns merged redaction fields (default sensitive keys + configured)
|
|
770
|
-
#
|
|
771
|
-
# @return [Array<String>] Field names to redact
|
|
772
|
-
def redaction_fields
|
|
773
|
-
default_fields = %w[password token api_key secret credential auth key access_token]
|
|
774
|
-
configured_fields = redaction&.dig(:fields) || []
|
|
775
|
-
(default_fields + configured_fields).map(&:downcase).uniq
|
|
776
|
-
end
|
|
777
|
-
|
|
778
|
-
# Returns redaction patterns
|
|
779
|
-
#
|
|
780
|
-
# @return [Array<Regexp>] Patterns to match and redact
|
|
781
|
-
def redaction_patterns
|
|
782
|
-
redaction&.dig(:patterns) || []
|
|
783
|
-
end
|
|
784
|
-
|
|
785
|
-
# Returns the redaction placeholder string
|
|
786
|
-
#
|
|
787
|
-
# @return [String] Placeholder to replace redacted values
|
|
788
|
-
def redaction_placeholder
|
|
789
|
-
redaction&.dig(:placeholder) || "[REDACTED]"
|
|
790
|
-
end
|
|
791
|
-
|
|
792
|
-
# Returns the maximum value length before truncation
|
|
793
|
-
#
|
|
794
|
-
# @return [Integer, nil] Max length, or nil for no limit
|
|
795
|
-
def redaction_max_value_length
|
|
796
|
-
redaction&.dig(:max_value_length)
|
|
797
|
-
end
|
|
798
|
-
|
|
799
717
|
# Returns whether multi-tenancy is enabled
|
|
800
718
|
#
|
|
801
719
|
# @return [Boolean] true if multi-tenancy is enabled
|
|
@@ -869,8 +787,6 @@ module RubyLLM
|
|
|
869
787
|
when :images then "images"
|
|
870
788
|
when :audio then "audio"
|
|
871
789
|
when :embedders then "embedders"
|
|
872
|
-
when :moderators then "moderators"
|
|
873
|
-
when :workflows then "workflows"
|
|
874
790
|
when :text then "text"
|
|
875
791
|
when :image then "image"
|
|
876
792
|
end
|
|
@@ -895,16 +811,41 @@ module RubyLLM
|
|
|
895
811
|
|
|
896
812
|
[
|
|
897
813
|
base,
|
|
898
|
-
"app/workflows", # Top-level workflows directory
|
|
899
814
|
"#{base}/images",
|
|
900
815
|
"#{base}/audio",
|
|
901
816
|
"#{base}/embedders",
|
|
902
|
-
"#{base}/moderators",
|
|
903
|
-
"#{base}/workflows",
|
|
904
817
|
"#{base}/tools"
|
|
905
818
|
]
|
|
906
819
|
end
|
|
907
820
|
|
|
821
|
+
# Returns the redaction fields (parameter names to redact)
|
|
822
|
+
#
|
|
823
|
+
# @return [Array<String>] Fields to redact
|
|
824
|
+
def redaction_fields
|
|
825
|
+
redaction&.dig(:fields) || []
|
|
826
|
+
end
|
|
827
|
+
|
|
828
|
+
# Returns the redaction regex patterns
|
|
829
|
+
#
|
|
830
|
+
# @return [Array<Regexp>] Patterns to match and redact
|
|
831
|
+
def redaction_patterns
|
|
832
|
+
redaction&.dig(:patterns) || []
|
|
833
|
+
end
|
|
834
|
+
|
|
835
|
+
# Returns the redaction placeholder string
|
|
836
|
+
#
|
|
837
|
+
# @return [String] Placeholder for redacted values (default: "[REDACTED]")
|
|
838
|
+
def redaction_placeholder
|
|
839
|
+
redaction&.dig(:placeholder) || "[REDACTED]"
|
|
840
|
+
end
|
|
841
|
+
|
|
842
|
+
# Returns the max value length for redaction
|
|
843
|
+
#
|
|
844
|
+
# @return [Integer, nil] Max length before truncation, or nil for no limit
|
|
845
|
+
def redaction_max_value_length
|
|
846
|
+
redaction&.dig(:max_value_length)
|
|
847
|
+
end
|
|
848
|
+
|
|
908
849
|
private
|
|
909
850
|
|
|
910
851
|
# Validates that a value is within a range
|