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,196 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
module Agents
|
|
5
|
+
module Pipeline
|
|
6
|
+
module Middleware
|
|
7
|
+
# Resolves tenant context from options and applies API configuration.
|
|
8
|
+
#
|
|
9
|
+
# This middleware extracts tenant information from the context options,
|
|
10
|
+
# sets the tenant_id, tenant_object, and tenant_config on the context,
|
|
11
|
+
# and applies the resolved API configuration to RubyLLM.
|
|
12
|
+
#
|
|
13
|
+
# Supports three formats:
|
|
14
|
+
# - Object with llm_tenant_id method (recommended for ActiveRecord models)
|
|
15
|
+
# - Hash with :id key (simple/legacy format)
|
|
16
|
+
# - nil (no tenant - single-tenant mode)
|
|
17
|
+
#
|
|
18
|
+
# API key resolution priority:
|
|
19
|
+
# 1. Tenant object's llm_api_keys method (if present)
|
|
20
|
+
# 2. Tenant-specific database config (ApiConfiguration)
|
|
21
|
+
# 3. Global database config
|
|
22
|
+
# 4. RubyLLM.configuration (set via initializer or environment)
|
|
23
|
+
#
|
|
24
|
+
# @example With ActiveRecord model
|
|
25
|
+
# # Model uses llm_tenant DSL
|
|
26
|
+
# class Organization < ApplicationRecord
|
|
27
|
+
# include RubyLLM::Agents::LLMTenant
|
|
28
|
+
# llm_tenant id: :slug, api_keys: { openai: :openai_key }
|
|
29
|
+
# end
|
|
30
|
+
#
|
|
31
|
+
# # Pass tenant to agent
|
|
32
|
+
# MyAgent.call(query: "test", tenant: organization)
|
|
33
|
+
#
|
|
34
|
+
# @example With hash
|
|
35
|
+
# MyAgent.call(query: "test", tenant: { id: "org_123" })
|
|
36
|
+
#
|
|
37
|
+
# @example Without tenant
|
|
38
|
+
# MyAgent.call(query: "test") # Single-tenant mode
|
|
39
|
+
#
|
|
40
|
+
class Tenant < Base
|
|
41
|
+
# Process tenant resolution and API key application
|
|
42
|
+
#
|
|
43
|
+
# @param context [Context] The execution context
|
|
44
|
+
# @return [Context] The context with tenant fields populated
|
|
45
|
+
def call(context)
|
|
46
|
+
resolve_tenant!(context)
|
|
47
|
+
apply_api_configuration!(context)
|
|
48
|
+
@app.call(context)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
private
|
|
52
|
+
|
|
53
|
+
# Resolves tenant context from options
|
|
54
|
+
#
|
|
55
|
+
# @param context [Context] The execution context
|
|
56
|
+
# @raise [ArgumentError] If tenant format is invalid
|
|
57
|
+
def resolve_tenant!(context)
|
|
58
|
+
tenant_value = context.options[:tenant]
|
|
59
|
+
|
|
60
|
+
case tenant_value
|
|
61
|
+
when nil
|
|
62
|
+
# No tenant - single-tenant mode
|
|
63
|
+
context.tenant_id = nil
|
|
64
|
+
context.tenant_object = nil
|
|
65
|
+
context.tenant_config = nil
|
|
66
|
+
|
|
67
|
+
when Hash
|
|
68
|
+
# Hash format: { id: "tenant_id", object: tenant, ... }
|
|
69
|
+
# The :object key is set by BaseAgent.resolve_tenant when tenant object
|
|
70
|
+
# is passed via tenant: param
|
|
71
|
+
context.tenant_id = tenant_value[:id]&.to_s
|
|
72
|
+
context.tenant_object = tenant_value[:object]
|
|
73
|
+
context.tenant_config = tenant_value.except(:id, :object)
|
|
74
|
+
|
|
75
|
+
else
|
|
76
|
+
# Object with llm_tenant_id method
|
|
77
|
+
if tenant_value.respond_to?(:llm_tenant_id)
|
|
78
|
+
context.tenant_id = tenant_value.llm_tenant_id&.to_s
|
|
79
|
+
context.tenant_object = tenant_value
|
|
80
|
+
context.tenant_config = extract_tenant_config(tenant_value)
|
|
81
|
+
else
|
|
82
|
+
raise ArgumentError,
|
|
83
|
+
"tenant must respond to :llm_tenant_id (use llm_tenant DSL), " \
|
|
84
|
+
"or be a Hash with :id key, got #{tenant_value.class}"
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Applies API configuration to RubyLLM based on resolved tenant
|
|
90
|
+
#
|
|
91
|
+
# This method resolves API keys from multiple sources and applies
|
|
92
|
+
# them to RubyLLM.config before the agent executes.
|
|
93
|
+
#
|
|
94
|
+
# @param context [Context] The execution context
|
|
95
|
+
def apply_api_configuration!(context)
|
|
96
|
+
# First, try to apply keys from tenant object's llm_api_keys method
|
|
97
|
+
apply_tenant_object_api_keys!(context)
|
|
98
|
+
|
|
99
|
+
# Then, apply database configuration (tenant > global > ruby_llm_config)
|
|
100
|
+
apply_database_api_configuration!(context)
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# Applies API keys from tenant object's llm_api_keys method
|
|
104
|
+
#
|
|
105
|
+
# @param context [Context] The execution context
|
|
106
|
+
def apply_tenant_object_api_keys!(context)
|
|
107
|
+
tenant_object = context.tenant_object
|
|
108
|
+
return unless tenant_object.respond_to?(:llm_api_keys)
|
|
109
|
+
|
|
110
|
+
api_keys = tenant_object.llm_api_keys
|
|
111
|
+
return if api_keys.blank?
|
|
112
|
+
|
|
113
|
+
apply_api_keys_to_ruby_llm(api_keys)
|
|
114
|
+
rescue StandardError => e
|
|
115
|
+
# Log but don't fail if API key extraction fails
|
|
116
|
+
warn_api_key_error("tenant object", e)
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# Applies API configuration from the database
|
|
120
|
+
#
|
|
121
|
+
# @param context [Context] The execution context
|
|
122
|
+
def apply_database_api_configuration!(context)
|
|
123
|
+
return unless api_configuration_available?
|
|
124
|
+
|
|
125
|
+
resolved = ApiConfiguration.resolve(tenant_id: context.tenant_id)
|
|
126
|
+
resolved.apply_to_ruby_llm!
|
|
127
|
+
|
|
128
|
+
# Store resolved config on context for observability
|
|
129
|
+
context[:resolved_api_config] = resolved
|
|
130
|
+
rescue StandardError => e
|
|
131
|
+
# Log but don't fail if DB lookup fails
|
|
132
|
+
warn_api_key_error("database", e)
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# Applies a hash of API keys to RubyLLM configuration
|
|
136
|
+
#
|
|
137
|
+
# @param api_keys [Hash] Hash of provider => key mappings
|
|
138
|
+
def apply_api_keys_to_ruby_llm(api_keys)
|
|
139
|
+
RubyLLM.configure do |config|
|
|
140
|
+
api_keys.each do |provider, key|
|
|
141
|
+
next if key.blank?
|
|
142
|
+
|
|
143
|
+
setter = api_key_setter_for(provider)
|
|
144
|
+
config.public_send(setter, key) if config.respond_to?(setter)
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# Returns the setter method name for a provider's API key
|
|
150
|
+
#
|
|
151
|
+
# @param provider [Symbol, String] Provider name (e.g., :openai, :anthropic)
|
|
152
|
+
# @return [String] Setter method name (e.g., "openai_api_key=")
|
|
153
|
+
def api_key_setter_for(provider)
|
|
154
|
+
"#{provider}_api_key="
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
# Checks if ApiConfiguration model is available
|
|
158
|
+
#
|
|
159
|
+
# @return [Boolean]
|
|
160
|
+
def api_configuration_available?
|
|
161
|
+
return false unless defined?(RubyLLM::Agents::ApiConfiguration)
|
|
162
|
+
|
|
163
|
+
# Check if table exists
|
|
164
|
+
ApiConfiguration.table_exists?
|
|
165
|
+
rescue StandardError
|
|
166
|
+
false
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
# Logs a warning about API key resolution failure
|
|
170
|
+
#
|
|
171
|
+
# @param source [String] Source that failed
|
|
172
|
+
# @param error [StandardError] The error
|
|
173
|
+
def warn_api_key_error(source, error)
|
|
174
|
+
return unless defined?(Rails) && Rails.respond_to?(:logger)
|
|
175
|
+
|
|
176
|
+
Rails.logger.warn(
|
|
177
|
+
"[RubyLLM::Agents] Failed to resolve API keys from #{source}: #{error.message}"
|
|
178
|
+
)
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
# Extracts additional configuration from tenant object
|
|
182
|
+
#
|
|
183
|
+
# @param tenant [Object] The tenant object
|
|
184
|
+
# @return [Hash, nil] Additional configuration or nil
|
|
185
|
+
def extract_tenant_config(tenant)
|
|
186
|
+
return nil unless tenant.respond_to?(:llm_config)
|
|
187
|
+
|
|
188
|
+
tenant.llm_config
|
|
189
|
+
rescue StandardError
|
|
190
|
+
nil
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
end
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Pipeline infrastructure for middleware-based agent execution
|
|
4
|
+
#
|
|
5
|
+
# The pipeline provides a clean separation of concerns through middleware:
|
|
6
|
+
# - Context: Carries data through the pipeline
|
|
7
|
+
# - Middleware: Wraps execution with cross-cutting concerns
|
|
8
|
+
# - Builder: Constructs the middleware stack
|
|
9
|
+
# - Executor: Adapts agent execution to the pipeline interface
|
|
10
|
+
#
|
|
11
|
+
# @example Basic pipeline usage
|
|
12
|
+
# # Build a pipeline for an agent class
|
|
13
|
+
# pipeline = Pipeline::Builder.for(MyEmbedder).build(
|
|
14
|
+
# Pipeline::Executor.new(agent_instance)
|
|
15
|
+
# )
|
|
16
|
+
#
|
|
17
|
+
# # Create a context and execute
|
|
18
|
+
# context = Pipeline::Context.new(
|
|
19
|
+
# input: "Hello world",
|
|
20
|
+
# agent_class: MyEmbedder
|
|
21
|
+
# )
|
|
22
|
+
# result_context = pipeline.call(context)
|
|
23
|
+
#
|
|
24
|
+
# @see Pipeline::Context
|
|
25
|
+
# @see Pipeline::Builder
|
|
26
|
+
# @see Pipeline::Middleware::Base
|
|
27
|
+
#
|
|
28
|
+
require_relative "pipeline/context"
|
|
29
|
+
require_relative "pipeline/executor"
|
|
30
|
+
require_relative "pipeline/builder"
|
|
31
|
+
|
|
32
|
+
# Middleware classes
|
|
33
|
+
require_relative "pipeline/middleware/base"
|
|
34
|
+
require_relative "pipeline/middleware/tenant"
|
|
35
|
+
require_relative "pipeline/middleware/budget"
|
|
36
|
+
require_relative "pipeline/middleware/cache"
|
|
37
|
+
require_relative "pipeline/middleware/instrumentation"
|
|
38
|
+
require_relative "pipeline/middleware/reliability"
|
|
39
|
+
|
|
40
|
+
module RubyLLM
|
|
41
|
+
module Agents
|
|
42
|
+
module Pipeline
|
|
43
|
+
class << self
|
|
44
|
+
# Build a pipeline for an agent class with default middleware
|
|
45
|
+
#
|
|
46
|
+
# This is a convenience method that combines Builder.for with build.
|
|
47
|
+
#
|
|
48
|
+
# @param agent_class [Class] The agent class
|
|
49
|
+
# @param executor [#call] The core executor
|
|
50
|
+
# @return [#call] The complete pipeline
|
|
51
|
+
def build(agent_class, executor)
|
|
52
|
+
Builder.for(agent_class).build(executor)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Build an empty pipeline (no middleware)
|
|
56
|
+
#
|
|
57
|
+
# Useful for testing or when you want direct execution.
|
|
58
|
+
#
|
|
59
|
+
# @param agent_class [Class] The agent class
|
|
60
|
+
# @param executor [#call] The core executor
|
|
61
|
+
# @return [#call] The executor (no middleware wrapping)
|
|
62
|
+
def build_empty(agent_class, executor)
|
|
63
|
+
Builder.empty(agent_class).build(executor)
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
@@ -32,10 +32,11 @@ module RubyLLM
|
|
|
32
32
|
#
|
|
33
33
|
# @api private
|
|
34
34
|
config.to_prepare do
|
|
35
|
-
require_relative "execution_logger_job"
|
|
36
|
-
require_relative "instrumentation"
|
|
37
|
-
require_relative "
|
|
38
|
-
require_relative "
|
|
35
|
+
require_relative "../infrastructure/execution_logger_job"
|
|
36
|
+
require_relative "../core/instrumentation"
|
|
37
|
+
require_relative "../core/resolved_config"
|
|
38
|
+
require_relative "../core/base"
|
|
39
|
+
require_relative "../workflow/orchestrator"
|
|
39
40
|
|
|
40
41
|
# Resolve the parent controller class from configuration
|
|
41
42
|
# Default is ActionController::Base, but can be set to inherit from app controllers
|
|
@@ -169,16 +170,84 @@ module RubyLLM
|
|
|
169
170
|
g.factory_bot dir: "spec/factories"
|
|
170
171
|
end
|
|
171
172
|
|
|
172
|
-
# Adds the host app's
|
|
173
|
+
# Adds the host app's LLM directories to Rails autoload paths
|
|
173
174
|
#
|
|
174
|
-
# This allows agent classes defined in app/
|
|
175
|
-
# loaded without explicit requires.
|
|
175
|
+
# This allows agent classes and other LLM components defined in app/llm/
|
|
176
|
+
# to be automatically loaded without explicit requires.
|
|
177
|
+
#
|
|
178
|
+
# Supports two structures:
|
|
179
|
+
# 1. New grouped structure: app/llm/agents/, app/llm/tools/, etc.
|
|
180
|
+
# 2. Legacy flat structure: app/agents/ (for backwards compatibility)
|
|
176
181
|
#
|
|
177
182
|
# @api private
|
|
178
183
|
initializer "ruby_llm_agents.autoload_agents", before: :set_autoload_paths do |app|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
184
|
+
config = RubyLLM::Agents.configuration
|
|
185
|
+
|
|
186
|
+
# Check for new grouped structure (app/llm/*)
|
|
187
|
+
root_path = app.root.join("app", config.root_directory)
|
|
188
|
+
if root_path.exist?
|
|
189
|
+
# Add each configured path that exists
|
|
190
|
+
config.all_autoload_paths.each do |relative_path|
|
|
191
|
+
full_path = app.root.join(relative_path)
|
|
192
|
+
if full_path.exist?
|
|
193
|
+
# Configure namespace for the path
|
|
194
|
+
namespace = self.class.namespace_for_path(relative_path, config)
|
|
195
|
+
if namespace
|
|
196
|
+
Rails.autoloaders.main.push_dir(full_path.to_s, namespace: namespace)
|
|
197
|
+
else
|
|
198
|
+
Rails.autoloaders.main.push_dir(full_path.to_s)
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
else
|
|
203
|
+
# Fallback to legacy flat structure (app/agents/)
|
|
204
|
+
agents_path = app.root.join("app/agents")
|
|
205
|
+
if agents_path.exist?
|
|
206
|
+
Rails.autoloaders.main.push_dir(agents_path.to_s)
|
|
207
|
+
end
|
|
208
|
+
end
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
# Determines the namespace constant for a given path
|
|
212
|
+
#
|
|
213
|
+
# @param path [String] Relative path like "app/llm/agents"
|
|
214
|
+
# @param config [Configuration] Current configuration
|
|
215
|
+
# @return [Module, nil] Namespace module or nil for top-level
|
|
216
|
+
# @api private
|
|
217
|
+
def self.namespace_for_path(path, config)
|
|
218
|
+
# Parse the path to determine namespace
|
|
219
|
+
parts = path.split("/")
|
|
220
|
+
return nil unless parts.length >= 3
|
|
221
|
+
|
|
222
|
+
category = parts[2] # e.g., "agents", "audio", "image", "text"
|
|
223
|
+
|
|
224
|
+
# Determine the namespace name based on category and root_namespace setting
|
|
225
|
+
namespace_name = if config.root_namespace.blank?
|
|
226
|
+
# No root namespace - use category namespace only for audio/image/text
|
|
227
|
+
case category
|
|
228
|
+
when "audio", "image", "text"
|
|
229
|
+
category.camelize # "Audio", "Image", "Text"
|
|
230
|
+
else
|
|
231
|
+
nil # Top-level for agents, workflows, tools
|
|
232
|
+
end
|
|
233
|
+
else
|
|
234
|
+
# With root namespace - prefix category with root namespace
|
|
235
|
+
case category
|
|
236
|
+
when "audio", "image", "text"
|
|
237
|
+
"#{config.root_namespace}::#{category.camelize}"
|
|
238
|
+
else
|
|
239
|
+
config.root_namespace
|
|
240
|
+
end
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
return nil if namespace_name.nil?
|
|
244
|
+
|
|
245
|
+
# Return the constant, creating intermediate modules if needed
|
|
246
|
+
namespace_name.constantize
|
|
247
|
+
rescue NameError
|
|
248
|
+
# Create the namespace module if it doesn't exist
|
|
249
|
+
namespace_name.split("::").inject(Object) do |mod, name|
|
|
250
|
+
mod.const_defined?(name, false) ? mod.const_get(name) : mod.const_set(name, Module.new)
|
|
182
251
|
end
|
|
183
252
|
end
|
|
184
253
|
end
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
module Agents
|
|
5
|
+
# Result wrapper for background removal operations
|
|
6
|
+
#
|
|
7
|
+
# Provides a consistent interface for accessing the extracted foreground,
|
|
8
|
+
# segmentation mask, and metadata.
|
|
9
|
+
#
|
|
10
|
+
# @example Accessing removal result
|
|
11
|
+
# result = BackgroundRemover.call(image: "photo.jpg")
|
|
12
|
+
# result.url # => "https://..." (transparent PNG)
|
|
13
|
+
# result.has_alpha? # => true
|
|
14
|
+
# result.mask # => Segmentation mask (if requested)
|
|
15
|
+
# result.success? # => true
|
|
16
|
+
#
|
|
17
|
+
class BackgroundRemovalResult
|
|
18
|
+
attr_reader :foreground, :mask, :source_image, :model_id, :output_format,
|
|
19
|
+
:alpha_matting, :refine_edges,
|
|
20
|
+
:started_at, :completed_at, :tenant_id, :remover_class,
|
|
21
|
+
:error_class, :error_message
|
|
22
|
+
|
|
23
|
+
# Initialize a new result
|
|
24
|
+
#
|
|
25
|
+
# @param foreground [Object] The extracted foreground image
|
|
26
|
+
# @param mask [Object, nil] The segmentation mask (if requested)
|
|
27
|
+
# @param source_image [String] The original source image
|
|
28
|
+
# @param model_id [String] Model used for removal
|
|
29
|
+
# @param output_format [Symbol] Output format used
|
|
30
|
+
# @param alpha_matting [Boolean] Whether alpha matting was used
|
|
31
|
+
# @param refine_edges [Boolean] Whether edge refinement was used
|
|
32
|
+
# @param started_at [Time] When removal started
|
|
33
|
+
# @param completed_at [Time] When removal completed
|
|
34
|
+
# @param tenant_id [String, nil] Tenant identifier
|
|
35
|
+
# @param remover_class [String] Name of the remover class
|
|
36
|
+
# @param error_class [String, nil] Error class name if failed
|
|
37
|
+
# @param error_message [String, nil] Error message if failed
|
|
38
|
+
def initialize(foreground:, mask:, source_image:, model_id:, output_format:,
|
|
39
|
+
alpha_matting:, refine_edges:, started_at:, completed_at:,
|
|
40
|
+
tenant_id:, remover_class:, error_class: nil, error_message: nil)
|
|
41
|
+
@foreground = foreground
|
|
42
|
+
@mask = mask
|
|
43
|
+
@source_image = source_image
|
|
44
|
+
@model_id = model_id
|
|
45
|
+
@output_format = output_format
|
|
46
|
+
@alpha_matting = alpha_matting
|
|
47
|
+
@refine_edges = refine_edges
|
|
48
|
+
@started_at = started_at
|
|
49
|
+
@completed_at = completed_at
|
|
50
|
+
@tenant_id = tenant_id
|
|
51
|
+
@remover_class = remover_class
|
|
52
|
+
@error_class = error_class
|
|
53
|
+
@error_message = error_message
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Status helpers
|
|
57
|
+
|
|
58
|
+
def success?
|
|
59
|
+
error_class.nil? && !foreground.nil?
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def error?
|
|
63
|
+
!success?
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Always single for background removal
|
|
67
|
+
def single?
|
|
68
|
+
true
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def batch?
|
|
72
|
+
false
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Image access (foreground)
|
|
76
|
+
|
|
77
|
+
def image
|
|
78
|
+
foreground
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def url
|
|
82
|
+
foreground&.url
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def urls
|
|
86
|
+
success? ? [url].compact : []
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def data
|
|
90
|
+
foreground&.data
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def datas
|
|
94
|
+
success? ? [data].compact : []
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def base64?
|
|
98
|
+
foreground&.base64? || false
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def mime_type
|
|
102
|
+
foreground&.mime_type || "image/#{output_format}"
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# Mask access
|
|
106
|
+
|
|
107
|
+
def mask?
|
|
108
|
+
!mask.nil?
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def mask_url
|
|
112
|
+
mask&.url
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def mask_data
|
|
116
|
+
mask&.data
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# Check if result has alpha channel (transparency)
|
|
120
|
+
def has_alpha?
|
|
121
|
+
return false if error?
|
|
122
|
+
|
|
123
|
+
# PNG and WebP support alpha
|
|
124
|
+
%i[png webp].include?(output_format)
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# Count (always 1 for removal)
|
|
128
|
+
|
|
129
|
+
def count
|
|
130
|
+
success? ? 1 : 0
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# Timing
|
|
134
|
+
|
|
135
|
+
def duration_ms
|
|
136
|
+
return 0 unless started_at && completed_at
|
|
137
|
+
((completed_at - started_at) * 1000).round
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# Cost estimation
|
|
141
|
+
|
|
142
|
+
def total_cost
|
|
143
|
+
return 0 if error?
|
|
144
|
+
|
|
145
|
+
# Background removal typically has fixed per-image cost
|
|
146
|
+
ImageGenerator::Pricing.calculate_cost(
|
|
147
|
+
model_id: model_id,
|
|
148
|
+
count: 1
|
|
149
|
+
)
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
# File operations
|
|
153
|
+
|
|
154
|
+
def save(path)
|
|
155
|
+
raise "No foreground image to save" unless foreground
|
|
156
|
+
foreground.save(path)
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def save_mask(path)
|
|
160
|
+
raise "No mask to save" unless mask
|
|
161
|
+
mask.save(path)
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def to_blob
|
|
165
|
+
foreground&.to_blob
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def mask_blob
|
|
169
|
+
mask&.to_blob
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def blobs
|
|
173
|
+
success? ? [to_blob].compact : []
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
# Serialization
|
|
177
|
+
|
|
178
|
+
def to_h
|
|
179
|
+
{
|
|
180
|
+
success: success?,
|
|
181
|
+
url: url,
|
|
182
|
+
mask_url: mask_url,
|
|
183
|
+
base64: base64?,
|
|
184
|
+
mime_type: mime_type,
|
|
185
|
+
has_alpha: has_alpha?,
|
|
186
|
+
has_mask: mask?,
|
|
187
|
+
source_image: source_image,
|
|
188
|
+
model_id: model_id,
|
|
189
|
+
output_format: output_format,
|
|
190
|
+
alpha_matting: alpha_matting,
|
|
191
|
+
refine_edges: refine_edges,
|
|
192
|
+
total_cost: total_cost,
|
|
193
|
+
duration_ms: duration_ms,
|
|
194
|
+
started_at: started_at&.iso8601,
|
|
195
|
+
completed_at: completed_at&.iso8601,
|
|
196
|
+
tenant_id: tenant_id,
|
|
197
|
+
remover_class: remover_class,
|
|
198
|
+
error_class: error_class,
|
|
199
|
+
error_message: error_message
|
|
200
|
+
}
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
# Caching
|
|
204
|
+
|
|
205
|
+
def to_cache
|
|
206
|
+
{
|
|
207
|
+
url: url,
|
|
208
|
+
data: data,
|
|
209
|
+
mask_url: mask_url,
|
|
210
|
+
mask_data: mask_data,
|
|
211
|
+
mime_type: mime_type,
|
|
212
|
+
model_id: model_id,
|
|
213
|
+
output_format: output_format,
|
|
214
|
+
total_cost: total_cost,
|
|
215
|
+
cached_at: Time.current.iso8601
|
|
216
|
+
}
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
def self.from_cache(data)
|
|
220
|
+
CachedBackgroundRemovalResult.new(data)
|
|
221
|
+
end
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
# Lightweight result for cached removals
|
|
225
|
+
class CachedBackgroundRemovalResult
|
|
226
|
+
attr_reader :url, :data, :mask_url, :mask_data, :mime_type, :model_id,
|
|
227
|
+
:output_format, :total_cost, :cached_at
|
|
228
|
+
|
|
229
|
+
def initialize(data)
|
|
230
|
+
@url = data[:url]
|
|
231
|
+
@data = data[:data]
|
|
232
|
+
@mask_url = data[:mask_url]
|
|
233
|
+
@mask_data = data[:mask_data]
|
|
234
|
+
@mime_type = data[:mime_type]
|
|
235
|
+
@model_id = data[:model_id]
|
|
236
|
+
@output_format = data[:output_format]
|
|
237
|
+
@total_cost = data[:total_cost]
|
|
238
|
+
@cached_at = data[:cached_at]
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
def success?
|
|
242
|
+
!url.nil? || !data.nil?
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
def error?
|
|
246
|
+
!success?
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
def cached?
|
|
250
|
+
true
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
def urls
|
|
254
|
+
success? ? [url].compact : []
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
def datas
|
|
258
|
+
success? ? [data].compact : []
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
def base64?
|
|
262
|
+
!data.nil?
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
def mask?
|
|
266
|
+
!mask_url.nil? || !mask_data.nil?
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
def has_alpha?
|
|
270
|
+
%i[png webp].include?(output_format&.to_sym)
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
def count
|
|
274
|
+
success? ? 1 : 0
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
def single?
|
|
278
|
+
true
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
def batch?
|
|
282
|
+
false
|
|
283
|
+
end
|
|
284
|
+
end
|
|
285
|
+
end
|
|
286
|
+
end
|