ruby_llm-agents 0.5.0 → 1.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 +189 -31
- data/app/controllers/ruby_llm/agents/agents_controller.rb +136 -16
- data/app/controllers/ruby_llm/agents/dashboard_controller.rb +29 -9
- 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/execution.rb +3 -0
- data/app/models/ruby_llm/agents/tenant_budget.rb +58 -15
- data/app/services/ruby_llm/agents/agent_registry.rb +51 -12
- data/app/views/layouts/ruby_llm/agents/application.html.erb +2 -29
- 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/dashboard/_agent_comparison.html.erb +269 -15
- data/app/views/ruby_llm/agents/executions/show.html.erb +16 -0
- data/app/views/ruby_llm/agents/shared/_agent_type_badge.html.erb +93 -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 +1 -0
- data/lib/generators/ruby_llm_agents/agent_generator.rb +56 -7
- 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/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} +22 -1
- data/lib/ruby_llm/agents/core/llm_tenant.rb +358 -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 -11
- 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 +172 -34
- 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 -366
- 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 -210
- data/lib/ruby_llm/agents/budget_tracker.rb +0 -733
- data/lib/ruby_llm/agents/configuration.rb +0 -394
- /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/{resolved_config.rb → core/resolved_config.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
|
@@ -1,82 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module RubyLLM
|
|
4
|
-
module Agents
|
|
5
|
-
class Base
|
|
6
|
-
# DSL builder for reliability configuration
|
|
7
|
-
#
|
|
8
|
-
# Provides a block-based configuration syntax for grouping
|
|
9
|
-
# all reliability settings together.
|
|
10
|
-
#
|
|
11
|
-
# @example Basic usage
|
|
12
|
-
# class MyAgent < ApplicationAgent
|
|
13
|
-
# reliability do
|
|
14
|
-
# retries max: 3, backoff: :exponential
|
|
15
|
-
# fallback_models "gpt-4o-mini"
|
|
16
|
-
# total_timeout 30
|
|
17
|
-
# circuit_breaker errors: 5, within: 60
|
|
18
|
-
# end
|
|
19
|
-
# end
|
|
20
|
-
#
|
|
21
|
-
# @api public
|
|
22
|
-
class ReliabilityDSL
|
|
23
|
-
attr_reader :retries_config, :fallback_models_list, :total_timeout_value, :circuit_breaker_config
|
|
24
|
-
|
|
25
|
-
def initialize
|
|
26
|
-
@retries_config = nil
|
|
27
|
-
@fallback_models_list = []
|
|
28
|
-
@total_timeout_value = nil
|
|
29
|
-
@circuit_breaker_config = nil
|
|
30
|
-
end
|
|
31
|
-
|
|
32
|
-
# Configures retry behavior
|
|
33
|
-
#
|
|
34
|
-
# @param max [Integer] Maximum retry attempts
|
|
35
|
-
# @param backoff [Symbol] :constant or :exponential
|
|
36
|
-
# @param base [Float] Base delay in seconds
|
|
37
|
-
# @param max_delay [Float] Maximum delay between retries
|
|
38
|
-
# @param on [Array<Class>] Additional error classes to retry on
|
|
39
|
-
# @return [void]
|
|
40
|
-
def retries(max: 0, backoff: :exponential, base: 0.4, max_delay: 3.0, on: [])
|
|
41
|
-
@retries_config = {
|
|
42
|
-
max: max,
|
|
43
|
-
backoff: backoff,
|
|
44
|
-
base: base,
|
|
45
|
-
max_delay: max_delay,
|
|
46
|
-
on: on
|
|
47
|
-
}
|
|
48
|
-
end
|
|
49
|
-
|
|
50
|
-
# Sets fallback models
|
|
51
|
-
#
|
|
52
|
-
# @param models [Array<String>] Model identifiers
|
|
53
|
-
# @return [void]
|
|
54
|
-
def fallback_models(*models)
|
|
55
|
-
@fallback_models_list = models.flatten
|
|
56
|
-
end
|
|
57
|
-
|
|
58
|
-
# Sets total timeout across all retry/fallback attempts
|
|
59
|
-
#
|
|
60
|
-
# @param seconds [Integer] Total timeout in seconds
|
|
61
|
-
# @return [void]
|
|
62
|
-
def total_timeout(seconds)
|
|
63
|
-
@total_timeout_value = seconds
|
|
64
|
-
end
|
|
65
|
-
|
|
66
|
-
# Configures circuit breaker
|
|
67
|
-
#
|
|
68
|
-
# @param errors [Integer] Failure threshold
|
|
69
|
-
# @param within [Integer] Rolling window in seconds
|
|
70
|
-
# @param cooldown [Integer] Cooldown period in seconds
|
|
71
|
-
# @return [void]
|
|
72
|
-
def circuit_breaker(errors: 10, within: 60, cooldown: 300)
|
|
73
|
-
@circuit_breaker_config = {
|
|
74
|
-
errors: errors,
|
|
75
|
-
within: within,
|
|
76
|
-
cooldown: cooldown
|
|
77
|
-
}
|
|
78
|
-
end
|
|
79
|
-
end
|
|
80
|
-
end
|
|
81
|
-
end
|
|
82
|
-
end
|
|
@@ -1,136 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module RubyLLM
|
|
4
|
-
module Agents
|
|
5
|
-
class Base
|
|
6
|
-
# Reliability execution with retry/fallback/circuit breaker support
|
|
7
|
-
#
|
|
8
|
-
# Handles executing agents with automatic retries, model fallbacks,
|
|
9
|
-
# circuit breaker protection, and budget enforcement.
|
|
10
|
-
module ReliabilityExecution
|
|
11
|
-
# Executes the agent with retry/fallback/circuit breaker support
|
|
12
|
-
#
|
|
13
|
-
# @yield [chunk] Yields chunks when streaming is enabled
|
|
14
|
-
# @return [Object] The processed response
|
|
15
|
-
# @raise [Reliability::AllModelsExhaustedError] If all models fail
|
|
16
|
-
# @raise [Reliability::BudgetExceededError] If budget limits exceeded
|
|
17
|
-
# @raise [Reliability::TotalTimeoutError] If total timeout exceeded
|
|
18
|
-
def execute_with_reliability(&block)
|
|
19
|
-
config = reliability_config
|
|
20
|
-
models_to_try = [model, *config[:fallback_models]].uniq
|
|
21
|
-
total_deadline = config[:total_timeout] ? Time.current + config[:total_timeout] : nil
|
|
22
|
-
started_at = Time.current
|
|
23
|
-
|
|
24
|
-
# Get current tenant_id for multi-tenancy support
|
|
25
|
-
global_config = RubyLLM::Agents.configuration
|
|
26
|
-
tenant_id = global_config.multi_tenancy_enabled? ? global_config.current_tenant_id : nil
|
|
27
|
-
|
|
28
|
-
# Pre-check budget (tenant_id is resolved automatically if not passed)
|
|
29
|
-
BudgetTracker.check_budget!(self.class.name, tenant_id: tenant_id) if global_config.budgets_enabled?
|
|
30
|
-
|
|
31
|
-
instrument_execution_with_attempts(models_to_try: models_to_try) do |attempt_tracker|
|
|
32
|
-
last_error = nil
|
|
33
|
-
|
|
34
|
-
models_to_try.each do |current_model|
|
|
35
|
-
# Check circuit breaker (with tenant isolation if enabled)
|
|
36
|
-
breaker = get_circuit_breaker(current_model, tenant_id: tenant_id)
|
|
37
|
-
if breaker&.open?
|
|
38
|
-
attempt_tracker.record_short_circuit(current_model)
|
|
39
|
-
next
|
|
40
|
-
end
|
|
41
|
-
|
|
42
|
-
retries_remaining = config[:retries]&.dig(:max) || 0
|
|
43
|
-
attempt_index = 0
|
|
44
|
-
|
|
45
|
-
loop do
|
|
46
|
-
# Check total timeout
|
|
47
|
-
if total_deadline && Time.current > total_deadline
|
|
48
|
-
elapsed = Time.current - started_at
|
|
49
|
-
raise Reliability::TotalTimeoutError.new(config[:total_timeout], elapsed)
|
|
50
|
-
end
|
|
51
|
-
|
|
52
|
-
attempt = attempt_tracker.start_attempt(current_model)
|
|
53
|
-
|
|
54
|
-
begin
|
|
55
|
-
result = execute_single_attempt(model_override: current_model, &block)
|
|
56
|
-
attempt_tracker.complete_attempt(attempt, success: true, response: @last_response)
|
|
57
|
-
|
|
58
|
-
# Record success in circuit breaker
|
|
59
|
-
breaker&.record_success!
|
|
60
|
-
|
|
61
|
-
# Record budget spend (with tenant isolation if enabled)
|
|
62
|
-
if @last_response && global_config.budgets_enabled?
|
|
63
|
-
record_attempt_cost(attempt_tracker, tenant_id: tenant_id)
|
|
64
|
-
end
|
|
65
|
-
|
|
66
|
-
# Use throw instead of return to allow instrument_execution_with_attempts
|
|
67
|
-
# to properly complete the execution record before returning
|
|
68
|
-
throw :execution_success, result
|
|
69
|
-
|
|
70
|
-
rescue *retryable_errors(config) => e
|
|
71
|
-
last_error = e
|
|
72
|
-
attempt_tracker.complete_attempt(attempt, success: false, error: e)
|
|
73
|
-
breaker&.record_failure!
|
|
74
|
-
|
|
75
|
-
if retries_remaining > 0 && !past_deadline?(total_deadline)
|
|
76
|
-
retries_remaining -= 1
|
|
77
|
-
attempt_index += 1
|
|
78
|
-
retries_config = config[:retries] || {}
|
|
79
|
-
delay = Reliability.calculate_backoff(
|
|
80
|
-
strategy: retries_config[:backoff] || :exponential,
|
|
81
|
-
base: retries_config[:base] || 0.4,
|
|
82
|
-
max_delay: retries_config[:max_delay] || 3.0,
|
|
83
|
-
attempt: attempt_index
|
|
84
|
-
)
|
|
85
|
-
sleep(delay)
|
|
86
|
-
else
|
|
87
|
-
break # Move to next model
|
|
88
|
-
end
|
|
89
|
-
|
|
90
|
-
rescue StandardError => e
|
|
91
|
-
# Non-retryable error - record and move to next model
|
|
92
|
-
last_error = e
|
|
93
|
-
attempt_tracker.complete_attempt(attempt, success: false, error: e)
|
|
94
|
-
breaker&.record_failure!
|
|
95
|
-
break
|
|
96
|
-
end
|
|
97
|
-
end
|
|
98
|
-
end
|
|
99
|
-
|
|
100
|
-
# All models exhausted
|
|
101
|
-
raise Reliability::AllModelsExhaustedError.new(models_to_try, last_error)
|
|
102
|
-
end
|
|
103
|
-
end
|
|
104
|
-
|
|
105
|
-
# Returns the list of retryable error classes
|
|
106
|
-
#
|
|
107
|
-
# @param config [Hash] Reliability configuration
|
|
108
|
-
# @return [Array<Class>] Error classes to retry on
|
|
109
|
-
def retryable_errors(config)
|
|
110
|
-
custom_errors = config[:retries]&.dig(:on) || []
|
|
111
|
-
Reliability.default_retryable_errors + custom_errors
|
|
112
|
-
end
|
|
113
|
-
|
|
114
|
-
# Checks if the total deadline has passed
|
|
115
|
-
#
|
|
116
|
-
# @param deadline [Time, nil] The deadline
|
|
117
|
-
# @return [Boolean] true if past deadline
|
|
118
|
-
def past_deadline?(deadline)
|
|
119
|
-
deadline && Time.current > deadline
|
|
120
|
-
end
|
|
121
|
-
|
|
122
|
-
# Gets or creates a circuit breaker for a model
|
|
123
|
-
#
|
|
124
|
-
# @param model_id [String] The model identifier
|
|
125
|
-
# @param tenant_id [String, nil] Optional tenant identifier for multi-tenant isolation
|
|
126
|
-
# @return [CircuitBreaker, nil] The circuit breaker or nil if not configured
|
|
127
|
-
def get_circuit_breaker(model_id, tenant_id: nil)
|
|
128
|
-
config = reliability_config[:circuit_breaker]
|
|
129
|
-
return nil unless config
|
|
130
|
-
|
|
131
|
-
CircuitBreaker.from_config(self.class.name, model_id, config, tenant_id: tenant_id)
|
|
132
|
-
end
|
|
133
|
-
end
|
|
134
|
-
end
|
|
135
|
-
end
|
|
136
|
-
end
|
|
@@ -1,86 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module RubyLLM
|
|
4
|
-
module Agents
|
|
5
|
-
class Base
|
|
6
|
-
# Result object construction from LLM responses
|
|
7
|
-
#
|
|
8
|
-
# Handles building Result objects with full execution metadata
|
|
9
|
-
# including tokens, costs, timing, and tool calls.
|
|
10
|
-
module ResponseBuilding
|
|
11
|
-
# Builds a Result object from processed content and response metadata
|
|
12
|
-
#
|
|
13
|
-
# @param content [Hash, String] The processed response content
|
|
14
|
-
# @param response [RubyLLM::Message] The raw LLM response
|
|
15
|
-
# @return [Result] A Result object with full execution metadata
|
|
16
|
-
def build_result(content, response)
|
|
17
|
-
completed_at = Time.current
|
|
18
|
-
input_tokens = result_response_value(response, :input_tokens)
|
|
19
|
-
output_tokens = result_response_value(response, :output_tokens)
|
|
20
|
-
response_model_id = result_response_value(response, :model_id)
|
|
21
|
-
|
|
22
|
-
Result.new(
|
|
23
|
-
content: content,
|
|
24
|
-
input_tokens: input_tokens,
|
|
25
|
-
output_tokens: output_tokens,
|
|
26
|
-
cached_tokens: result_response_value(response, :cached_tokens, 0),
|
|
27
|
-
cache_creation_tokens: result_response_value(response, :cache_creation_tokens, 0),
|
|
28
|
-
model_id: model,
|
|
29
|
-
chosen_model_id: response_model_id || model,
|
|
30
|
-
temperature: temperature,
|
|
31
|
-
started_at: @execution_started_at,
|
|
32
|
-
completed_at: completed_at,
|
|
33
|
-
duration_ms: result_duration_ms(completed_at),
|
|
34
|
-
time_to_first_token_ms: @time_to_first_token_ms,
|
|
35
|
-
finish_reason: result_finish_reason(response),
|
|
36
|
-
streaming: self.class.streaming,
|
|
37
|
-
input_cost: result_input_cost(input_tokens, response_model_id),
|
|
38
|
-
output_cost: result_output_cost(output_tokens, response_model_id),
|
|
39
|
-
total_cost: result_total_cost(input_tokens, output_tokens, response_model_id),
|
|
40
|
-
tool_calls: @accumulated_tool_calls,
|
|
41
|
-
tool_calls_count: @accumulated_tool_calls.size
|
|
42
|
-
)
|
|
43
|
-
end
|
|
44
|
-
|
|
45
|
-
# Safely extracts a value from the response object
|
|
46
|
-
#
|
|
47
|
-
# @param response [Object] The response object
|
|
48
|
-
# @param method [Symbol] The method to call
|
|
49
|
-
# @param default [Object] Default value if method doesn't exist
|
|
50
|
-
# @return [Object] The extracted value or default
|
|
51
|
-
def result_response_value(response, method, default = nil)
|
|
52
|
-
return default unless response.respond_to?(method)
|
|
53
|
-
response.send(method) || default
|
|
54
|
-
end
|
|
55
|
-
|
|
56
|
-
# Calculates execution duration in milliseconds
|
|
57
|
-
#
|
|
58
|
-
# @param completed_at [Time] When execution completed
|
|
59
|
-
# @return [Integer, nil] Duration in ms or nil
|
|
60
|
-
def result_duration_ms(completed_at)
|
|
61
|
-
return nil unless @execution_started_at
|
|
62
|
-
((completed_at - @execution_started_at) * 1000).to_i
|
|
63
|
-
end
|
|
64
|
-
|
|
65
|
-
# Extracts finish reason from response
|
|
66
|
-
#
|
|
67
|
-
# @param response [Object] The response object
|
|
68
|
-
# @return [String, nil] Normalized finish reason
|
|
69
|
-
def result_finish_reason(response)
|
|
70
|
-
reason = result_response_value(response, :finish_reason) ||
|
|
71
|
-
result_response_value(response, :stop_reason)
|
|
72
|
-
return nil unless reason
|
|
73
|
-
|
|
74
|
-
# Normalize to standard values
|
|
75
|
-
case reason.to_s.downcase
|
|
76
|
-
when "stop", "end_turn" then "stop"
|
|
77
|
-
when "length", "max_tokens" then "length"
|
|
78
|
-
when "content_filter", "safety" then "content_filter"
|
|
79
|
-
when "tool_calls", "tool_use" then "tool_calls"
|
|
80
|
-
else "other"
|
|
81
|
-
end
|
|
82
|
-
end
|
|
83
|
-
end
|
|
84
|
-
end
|
|
85
|
-
end
|
|
86
|
-
end
|
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module RubyLLM
|
|
4
|
-
module Agents
|
|
5
|
-
class Base
|
|
6
|
-
# Tool call tracking for agent executions
|
|
7
|
-
#
|
|
8
|
-
# Handles accumulating and serializing tool calls made during
|
|
9
|
-
# an agent's execution cycle.
|
|
10
|
-
module ToolTracking
|
|
11
|
-
# Resets accumulated tool calls for a new execution
|
|
12
|
-
#
|
|
13
|
-
# @return [void]
|
|
14
|
-
def reset_accumulated_tool_calls!
|
|
15
|
-
@accumulated_tool_calls = []
|
|
16
|
-
end
|
|
17
|
-
|
|
18
|
-
# Extracts tool calls from all assistant messages in the conversation
|
|
19
|
-
#
|
|
20
|
-
# RubyLLM handles tool call loops internally. After ask() completes,
|
|
21
|
-
# the conversation history contains all intermediate assistant messages
|
|
22
|
-
# that had tool_calls. This method extracts those tool calls.
|
|
23
|
-
#
|
|
24
|
-
# @param client [RubyLLM::Chat] The chat client with conversation history
|
|
25
|
-
# @return [void]
|
|
26
|
-
def extract_tool_calls_from_client(client)
|
|
27
|
-
return unless client.respond_to?(:messages)
|
|
28
|
-
|
|
29
|
-
client.messages.each do |message|
|
|
30
|
-
next unless message.role == :assistant
|
|
31
|
-
next unless message.respond_to?(:tool_calls) && message.tool_calls.present?
|
|
32
|
-
|
|
33
|
-
message.tool_calls.each_value do |tool_call|
|
|
34
|
-
@accumulated_tool_calls << serialize_tool_call(tool_call)
|
|
35
|
-
end
|
|
36
|
-
end
|
|
37
|
-
end
|
|
38
|
-
|
|
39
|
-
# Serializes a single tool call to a hash
|
|
40
|
-
#
|
|
41
|
-
# @param tool_call [Object] The tool call object
|
|
42
|
-
# @return [Hash] Serialized tool call
|
|
43
|
-
def serialize_tool_call(tool_call)
|
|
44
|
-
if tool_call.respond_to?(:to_h)
|
|
45
|
-
tool_call.to_h.transform_keys(&:to_s)
|
|
46
|
-
else
|
|
47
|
-
{
|
|
48
|
-
"id" => tool_call.id,
|
|
49
|
-
"name" => tool_call.name,
|
|
50
|
-
"arguments" => tool_call.arguments
|
|
51
|
-
}
|
|
52
|
-
end
|
|
53
|
-
end
|
|
54
|
-
end
|
|
55
|
-
end
|
|
56
|
-
end
|
|
57
|
-
end
|
data/lib/ruby_llm/agents/base.rb
DELETED
|
@@ -1,210 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require_relative "base/dsl"
|
|
4
|
-
require_relative "base/caching"
|
|
5
|
-
require_relative "base/cost_calculation"
|
|
6
|
-
require_relative "base/tool_tracking"
|
|
7
|
-
require_relative "base/response_building"
|
|
8
|
-
require_relative "base/execution"
|
|
9
|
-
require_relative "base/reliability_execution"
|
|
10
|
-
|
|
11
|
-
module RubyLLM
|
|
12
|
-
module Agents
|
|
13
|
-
# Base class for LLM-powered agents
|
|
14
|
-
#
|
|
15
|
-
# Provides a DSL for configuring and executing agents that interact with
|
|
16
|
-
# large language models. Includes built-in support for caching, timeouts,
|
|
17
|
-
# structured output, and execution tracking.
|
|
18
|
-
#
|
|
19
|
-
# @example Creating an agent
|
|
20
|
-
# class SearchAgent < ApplicationAgent
|
|
21
|
-
# model "gpt-4o"
|
|
22
|
-
# temperature 0.0
|
|
23
|
-
# version "1.0"
|
|
24
|
-
# timeout 30
|
|
25
|
-
# cache 1.hour
|
|
26
|
-
#
|
|
27
|
-
# param :query, required: true
|
|
28
|
-
# param :limit, default: 10
|
|
29
|
-
#
|
|
30
|
-
# def system_prompt
|
|
31
|
-
# "You are a search assistant..."
|
|
32
|
-
# end
|
|
33
|
-
#
|
|
34
|
-
# def user_prompt
|
|
35
|
-
# "Search for: #{query}"
|
|
36
|
-
# end
|
|
37
|
-
# end
|
|
38
|
-
#
|
|
39
|
-
# @example Calling an agent
|
|
40
|
-
# SearchAgent.call(query: "red dress")
|
|
41
|
-
# SearchAgent.call(query: "red dress", dry_run: true) # Debug mode
|
|
42
|
-
# SearchAgent.call(query: "red dress", skip_cache: true) # Bypass cache
|
|
43
|
-
#
|
|
44
|
-
# @see RubyLLM::Agents::Instrumentation
|
|
45
|
-
# @api public
|
|
46
|
-
class Base
|
|
47
|
-
include Instrumentation
|
|
48
|
-
include Caching
|
|
49
|
-
include CostCalculation
|
|
50
|
-
include ToolTracking
|
|
51
|
-
include ResponseBuilding
|
|
52
|
-
include Execution
|
|
53
|
-
include ReliabilityExecution
|
|
54
|
-
|
|
55
|
-
extend DSL
|
|
56
|
-
|
|
57
|
-
class << self
|
|
58
|
-
# Factory method to instantiate and execute an agent
|
|
59
|
-
#
|
|
60
|
-
# @param args [Array] Positional arguments (reserved for future use)
|
|
61
|
-
# @param kwargs [Hash] Named parameters for the agent
|
|
62
|
-
# @option kwargs [Boolean] :dry_run Return prompt info without API call
|
|
63
|
-
# @option kwargs [Boolean] :skip_cache Bypass caching even if enabled
|
|
64
|
-
# @option kwargs [String, Array<String>] :with Attachments (files, URLs) to send with the prompt
|
|
65
|
-
# @yield [chunk] Yields chunks when streaming is enabled
|
|
66
|
-
# @yieldparam chunk [RubyLLM::Chunk] A streaming chunk with content
|
|
67
|
-
# @return [Object] The processed response from the agent
|
|
68
|
-
#
|
|
69
|
-
# @example Basic usage
|
|
70
|
-
# SearchAgent.call(query: "red dress")
|
|
71
|
-
#
|
|
72
|
-
# @example Debug mode
|
|
73
|
-
# SearchAgent.call(query: "red dress", dry_run: true)
|
|
74
|
-
#
|
|
75
|
-
# @example Streaming mode
|
|
76
|
-
# ChatAgent.call(message: "Hello") do |chunk|
|
|
77
|
-
# print chunk.content
|
|
78
|
-
# end
|
|
79
|
-
#
|
|
80
|
-
# @example With attachments
|
|
81
|
-
# VisionAgent.call(query: "Describe this image", with: "photo.jpg")
|
|
82
|
-
# VisionAgent.call(query: "Compare these", with: ["a.png", "b.png"])
|
|
83
|
-
def call(*args, **kwargs, &block)
|
|
84
|
-
new(*args, **kwargs).call(&block)
|
|
85
|
-
end
|
|
86
|
-
|
|
87
|
-
# Streams agent execution, yielding chunks as they arrive
|
|
88
|
-
#
|
|
89
|
-
# A more explicit alternative to passing a block to call.
|
|
90
|
-
# Forces streaming mode for this invocation regardless of class setting.
|
|
91
|
-
#
|
|
92
|
-
# @param kwargs [Hash] Agent parameters
|
|
93
|
-
# @yield [chunk] Yields each chunk as it arrives
|
|
94
|
-
# @yieldparam chunk [RubyLLM::Chunk] Streaming chunk with content
|
|
95
|
-
# @return [Result] The final result after streaming completes
|
|
96
|
-
# @raise [ArgumentError] If no block is provided
|
|
97
|
-
#
|
|
98
|
-
# @example Basic streaming
|
|
99
|
-
# MyAgent.stream(query: "test") do |chunk|
|
|
100
|
-
# print chunk.content
|
|
101
|
-
# end
|
|
102
|
-
#
|
|
103
|
-
# @example With result metadata
|
|
104
|
-
# result = MyAgent.stream(query: "test") { |c| print c.content }
|
|
105
|
-
# puts "\nTokens: #{result.total_tokens}"
|
|
106
|
-
def stream(**kwargs, &block)
|
|
107
|
-
raise ArgumentError, "Block required for streaming" unless block_given?
|
|
108
|
-
|
|
109
|
-
instance = new(**kwargs)
|
|
110
|
-
instance.instance_variable_set(:@force_streaming, true)
|
|
111
|
-
instance.call(&block)
|
|
112
|
-
end
|
|
113
|
-
end
|
|
114
|
-
|
|
115
|
-
# @!attribute [r] model
|
|
116
|
-
# @return [String] The LLM model being used
|
|
117
|
-
# @!attribute [r] temperature
|
|
118
|
-
# @return [Float] The temperature setting
|
|
119
|
-
# @!attribute [r] client
|
|
120
|
-
# @return [RubyLLM::Chat] The configured RubyLLM client
|
|
121
|
-
# @!attribute [r] time_to_first_token_ms
|
|
122
|
-
# @return [Integer, nil] Time to first token in milliseconds (streaming only)
|
|
123
|
-
# @!attribute [r] accumulated_tool_calls
|
|
124
|
-
# @return [Array<Hash>] Tool calls accumulated during execution
|
|
125
|
-
attr_reader :model, :temperature, :client, :time_to_first_token_ms, :accumulated_tool_calls
|
|
126
|
-
|
|
127
|
-
# Creates a new agent instance
|
|
128
|
-
#
|
|
129
|
-
# @param model [String] Override the class-level model setting
|
|
130
|
-
# @param temperature [Float] Override the class-level temperature
|
|
131
|
-
# @param options [Hash] Agent parameters defined via the param DSL
|
|
132
|
-
# @raise [ArgumentError] If required parameters are missing
|
|
133
|
-
def initialize(model: self.class.model, temperature: self.class.temperature, **options)
|
|
134
|
-
@model = model
|
|
135
|
-
@temperature = temperature
|
|
136
|
-
@options = options
|
|
137
|
-
@accumulated_tool_calls = []
|
|
138
|
-
validate_required_params!
|
|
139
|
-
resolve_tenant_context! # Resolve tenant before building client for API key resolution
|
|
140
|
-
@client = build_client
|
|
141
|
-
end
|
|
142
|
-
|
|
143
|
-
# @!group Template Methods (override in subclasses)
|
|
144
|
-
|
|
145
|
-
# User prompt to send to the LLM
|
|
146
|
-
#
|
|
147
|
-
# @abstract Subclasses must implement this method
|
|
148
|
-
# @return [String] The user prompt
|
|
149
|
-
# @raise [NotImplementedError] If not overridden in subclass
|
|
150
|
-
def user_prompt
|
|
151
|
-
raise NotImplementedError, "#{self.class} must implement #user_prompt"
|
|
152
|
-
end
|
|
153
|
-
|
|
154
|
-
# System prompt for LLM instructions
|
|
155
|
-
#
|
|
156
|
-
# @return [String, nil] System instructions, or nil for none
|
|
157
|
-
def system_prompt
|
|
158
|
-
nil
|
|
159
|
-
end
|
|
160
|
-
|
|
161
|
-
# Response schema for structured output
|
|
162
|
-
#
|
|
163
|
-
# @return [RubyLLM::Schema, nil] Schema definition, or nil for free-form
|
|
164
|
-
def schema
|
|
165
|
-
nil
|
|
166
|
-
end
|
|
167
|
-
|
|
168
|
-
# Conversation history for multi-turn conversations
|
|
169
|
-
#
|
|
170
|
-
# Override in subclass to provide conversation history.
|
|
171
|
-
# Messages will be added to the chat before the user_prompt.
|
|
172
|
-
#
|
|
173
|
-
# @return [Array<Hash>] Array of messages with :role and :content keys
|
|
174
|
-
# @example
|
|
175
|
-
# def messages
|
|
176
|
-
# [{ role: :user, content: "Hello" }, { role: :assistant, content: "Hi!" }]
|
|
177
|
-
# end
|
|
178
|
-
def messages
|
|
179
|
-
[]
|
|
180
|
-
end
|
|
181
|
-
|
|
182
|
-
# Post-processes the LLM response
|
|
183
|
-
#
|
|
184
|
-
# Override to transform the response before returning to the caller.
|
|
185
|
-
# Default implementation symbolizes hash keys.
|
|
186
|
-
#
|
|
187
|
-
# @param response [RubyLLM::Message] The raw response from the LLM
|
|
188
|
-
# @return [Object] The processed result
|
|
189
|
-
def process_response(response)
|
|
190
|
-
content = response.content
|
|
191
|
-
return content unless content.is_a?(Hash)
|
|
192
|
-
content.transform_keys(&:to_sym)
|
|
193
|
-
end
|
|
194
|
-
|
|
195
|
-
# @!endgroup
|
|
196
|
-
|
|
197
|
-
# Sets conversation history and rebuilds the client
|
|
198
|
-
#
|
|
199
|
-
# @param msgs [Array<Hash>] Messages with :role and :content keys
|
|
200
|
-
# @return [self] Returns self for chaining
|
|
201
|
-
# @example
|
|
202
|
-
# agent.with_messages([{ role: :user, content: "Hi" }]).call
|
|
203
|
-
def with_messages(msgs)
|
|
204
|
-
@override_messages = msgs
|
|
205
|
-
@client = build_client
|
|
206
|
-
self
|
|
207
|
-
end
|
|
208
|
-
end
|
|
209
|
-
end
|
|
210
|
-
end
|