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,124 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
module Agents
|
|
5
|
+
module Pipeline
|
|
6
|
+
module Middleware
|
|
7
|
+
# Base class for all middleware in the pipeline.
|
|
8
|
+
#
|
|
9
|
+
# Middleware wraps the next handler in the chain and can:
|
|
10
|
+
# - Modify the context before passing it down
|
|
11
|
+
# - Short-circuit the chain (e.g., return cached result)
|
|
12
|
+
# - Handle errors from downstream
|
|
13
|
+
# - Modify the context after the response
|
|
14
|
+
#
|
|
15
|
+
# Each middleware receives:
|
|
16
|
+
# - @app: The next handler in the chain (another middleware or the executor)
|
|
17
|
+
# - @agent_class: The agent class, for reading DSL configuration
|
|
18
|
+
#
|
|
19
|
+
# @example Simple pass-through middleware
|
|
20
|
+
# class Logger < Base
|
|
21
|
+
# def call(context)
|
|
22
|
+
# puts "Before: #{context.input}"
|
|
23
|
+
# result = @app.call(context)
|
|
24
|
+
# puts "After: #{context.output}"
|
|
25
|
+
# result
|
|
26
|
+
# end
|
|
27
|
+
# end
|
|
28
|
+
#
|
|
29
|
+
# @example Short-circuiting middleware
|
|
30
|
+
# class Cache < Base
|
|
31
|
+
# def call(context)
|
|
32
|
+
# if (cached = read_cache(context))
|
|
33
|
+
# context.output = cached
|
|
34
|
+
# context.cached = true
|
|
35
|
+
# return context
|
|
36
|
+
# end
|
|
37
|
+
# @app.call(context)
|
|
38
|
+
# end
|
|
39
|
+
# end
|
|
40
|
+
#
|
|
41
|
+
# @abstract Subclass and implement {#call}
|
|
42
|
+
#
|
|
43
|
+
class Base
|
|
44
|
+
# @param app [#call] The next handler in the chain
|
|
45
|
+
# @param agent_class [Class] The agent class (for reading DSL config)
|
|
46
|
+
def initialize(app, agent_class)
|
|
47
|
+
@app = app
|
|
48
|
+
@agent_class = agent_class
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Process the context through this middleware
|
|
52
|
+
#
|
|
53
|
+
# Subclasses must implement this method. The typical pattern is:
|
|
54
|
+
# 1. Do pre-processing on context
|
|
55
|
+
# 2. Call @app.call(context) to continue the chain
|
|
56
|
+
# 3. Do post-processing on context
|
|
57
|
+
# 4. Return context
|
|
58
|
+
#
|
|
59
|
+
# @param context [Context] The execution context
|
|
60
|
+
# @return [Context] The (possibly modified) context
|
|
61
|
+
# @raise [NotImplementedError] If not implemented by subclass
|
|
62
|
+
def call(context)
|
|
63
|
+
raise NotImplementedError, "#{self.class} must implement #call"
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
private
|
|
67
|
+
|
|
68
|
+
# Read configuration from agent class DSL
|
|
69
|
+
#
|
|
70
|
+
# Safely reads a DSL method value from the agent class,
|
|
71
|
+
# returning a default if the method doesn't exist.
|
|
72
|
+
#
|
|
73
|
+
# @param method [Symbol] DSL method name
|
|
74
|
+
# @param default [Object] Default value if not set
|
|
75
|
+
# @return [Object] The configuration value
|
|
76
|
+
def config(method, default = nil)
|
|
77
|
+
return default unless @agent_class
|
|
78
|
+
|
|
79
|
+
if @agent_class.respond_to?(method)
|
|
80
|
+
@agent_class.send(method)
|
|
81
|
+
else
|
|
82
|
+
default
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Check if a DSL option is enabled
|
|
87
|
+
#
|
|
88
|
+
# Convenience method for boolean DSL options.
|
|
89
|
+
#
|
|
90
|
+
# @param method [Symbol] DSL method name (e.g., :cache_enabled?)
|
|
91
|
+
# @return [Boolean]
|
|
92
|
+
def enabled?(method)
|
|
93
|
+
config(method, false) == true
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Returns the global configuration
|
|
97
|
+
#
|
|
98
|
+
# @return [Configuration] The RubyLLM::Agents configuration
|
|
99
|
+
def global_config
|
|
100
|
+
RubyLLM::Agents.configuration
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# Log a debug message if Rails logger is available
|
|
104
|
+
#
|
|
105
|
+
# @param message [String] The message to log
|
|
106
|
+
def debug(message)
|
|
107
|
+
return unless defined?(Rails) && Rails.logger
|
|
108
|
+
|
|
109
|
+
Rails.logger.debug("[RubyLLM::Agents::Pipeline] #{message}")
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# Log an error message if Rails logger is available
|
|
113
|
+
#
|
|
114
|
+
# @param message [String] The message to log
|
|
115
|
+
def error(message)
|
|
116
|
+
return unless defined?(Rails) && Rails.logger
|
|
117
|
+
|
|
118
|
+
Rails.logger.error("[RubyLLM::Agents::Pipeline] #{message}")
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
end
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
module Agents
|
|
5
|
+
module Pipeline
|
|
6
|
+
module Middleware
|
|
7
|
+
# Checks budget limits before execution and records spend after.
|
|
8
|
+
#
|
|
9
|
+
# This middleware integrates with the BudgetTracker to:
|
|
10
|
+
# - Check if budget limits are exceeded before execution
|
|
11
|
+
# - Record spend after successful execution
|
|
12
|
+
#
|
|
13
|
+
# Budget checking is skipped if:
|
|
14
|
+
# - Budgets are disabled globally in configuration
|
|
15
|
+
# - The result was served from cache (no API call was made)
|
|
16
|
+
#
|
|
17
|
+
# @example With budget enforcement
|
|
18
|
+
# # In config/initializers/ruby_llm_agents.rb
|
|
19
|
+
# RubyLLM::Agents.configure do |config|
|
|
20
|
+
# config.budgets_enabled = true
|
|
21
|
+
# end
|
|
22
|
+
#
|
|
23
|
+
# # Budget will be checked before execution
|
|
24
|
+
# MyAgent.call(query: "test", tenant: { id: "org_123" })
|
|
25
|
+
#
|
|
26
|
+
class Budget < Base
|
|
27
|
+
# Process budget checking and spend recording
|
|
28
|
+
#
|
|
29
|
+
# @param context [Context] The execution context
|
|
30
|
+
# @return [Context] The context after budget processing
|
|
31
|
+
# @raise [BudgetExceededError] If budget is exceeded with hard enforcement
|
|
32
|
+
def call(context)
|
|
33
|
+
return @app.call(context) unless budgets_enabled?
|
|
34
|
+
|
|
35
|
+
# Check budget before execution
|
|
36
|
+
check_budget!(context)
|
|
37
|
+
|
|
38
|
+
# Execute the chain
|
|
39
|
+
@app.call(context)
|
|
40
|
+
|
|
41
|
+
# Record spend after successful execution (if not cached)
|
|
42
|
+
record_spend!(context) if context.success? && !context.cached?
|
|
43
|
+
|
|
44
|
+
context
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
private
|
|
48
|
+
|
|
49
|
+
# Returns whether budgets are enabled globally
|
|
50
|
+
#
|
|
51
|
+
# @return [Boolean]
|
|
52
|
+
def budgets_enabled?
|
|
53
|
+
global_config.budgets_enabled?
|
|
54
|
+
rescue StandardError
|
|
55
|
+
false
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Checks budget before execution
|
|
59
|
+
#
|
|
60
|
+
# @param context [Context] The execution context
|
|
61
|
+
# @raise [BudgetExceededError] If budget exceeded with hard enforcement
|
|
62
|
+
def check_budget!(context)
|
|
63
|
+
BudgetTracker.check!(
|
|
64
|
+
agent_type: context.agent_class&.name,
|
|
65
|
+
tenant_id: context.tenant_id,
|
|
66
|
+
execution_type: context.agent_type&.to_s
|
|
67
|
+
)
|
|
68
|
+
rescue BudgetExceededError
|
|
69
|
+
# Re-raise budget errors
|
|
70
|
+
raise
|
|
71
|
+
rescue StandardError => e
|
|
72
|
+
# Log but don't fail on budget check errors
|
|
73
|
+
error("Budget check failed: #{e.message}")
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Records spend after execution
|
|
77
|
+
#
|
|
78
|
+
# @param context [Context] The execution context
|
|
79
|
+
def record_spend!(context)
|
|
80
|
+
return unless context.total_cost&.positive?
|
|
81
|
+
|
|
82
|
+
BudgetTracker.record_spend!(
|
|
83
|
+
tenant_id: context.tenant_id,
|
|
84
|
+
cost: context.total_cost,
|
|
85
|
+
tokens: context.total_tokens
|
|
86
|
+
)
|
|
87
|
+
rescue StandardError => e
|
|
88
|
+
# Log but don't fail on spend recording errors
|
|
89
|
+
error("Failed to record spend: #{e.message}")
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "digest"
|
|
4
|
+
|
|
5
|
+
module RubyLLM
|
|
6
|
+
module Agents
|
|
7
|
+
module Pipeline
|
|
8
|
+
module Middleware
|
|
9
|
+
# Caches results to avoid redundant API calls.
|
|
10
|
+
#
|
|
11
|
+
# This middleware provides caching for agent executions:
|
|
12
|
+
# - Checks cache before execution
|
|
13
|
+
# - Stores successful results in cache
|
|
14
|
+
# - Respects TTL configuration from agent DSL
|
|
15
|
+
#
|
|
16
|
+
# Caching is skipped if:
|
|
17
|
+
# - Caching is not enabled on the agent class (no cache_for DSL)
|
|
18
|
+
# - The cache store is not configured
|
|
19
|
+
#
|
|
20
|
+
# @example Enable caching on an agent
|
|
21
|
+
# class MyEmbedder < RubyLLM::Agents::Embedder
|
|
22
|
+
# model "text-embedding-3-small"
|
|
23
|
+
# cache_for 1.hour
|
|
24
|
+
# end
|
|
25
|
+
#
|
|
26
|
+
# @example Cache versioning
|
|
27
|
+
# class MyEmbedder < RubyLLM::Agents::Embedder
|
|
28
|
+
# model "text-embedding-3-small"
|
|
29
|
+
# version "2.0" # Change to invalidate cache
|
|
30
|
+
# cache_for 1.hour
|
|
31
|
+
# end
|
|
32
|
+
#
|
|
33
|
+
class Cache < Base
|
|
34
|
+
# Process caching
|
|
35
|
+
#
|
|
36
|
+
# @param context [Context] The execution context
|
|
37
|
+
# @return [Context] The context (possibly from cache)
|
|
38
|
+
def call(context)
|
|
39
|
+
return @app.call(context) unless cache_enabled?
|
|
40
|
+
|
|
41
|
+
cache_key = generate_cache_key(context)
|
|
42
|
+
|
|
43
|
+
# Skip cache read if skip_cache is true
|
|
44
|
+
unless context.skip_cache
|
|
45
|
+
# Try to read from cache
|
|
46
|
+
if (cached = cache_read(cache_key))
|
|
47
|
+
context.output = cached
|
|
48
|
+
context.cached = true
|
|
49
|
+
context[:cache_key] = cache_key
|
|
50
|
+
debug("Cache hit for #{cache_key}")
|
|
51
|
+
return context
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Execute the chain
|
|
56
|
+
@app.call(context)
|
|
57
|
+
|
|
58
|
+
# Cache successful results
|
|
59
|
+
if context.success?
|
|
60
|
+
cache_write(cache_key, context.output)
|
|
61
|
+
debug("Cache write for #{cache_key}")
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
context
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
private
|
|
68
|
+
|
|
69
|
+
# Returns whether caching is enabled for this agent
|
|
70
|
+
#
|
|
71
|
+
# @return [Boolean]
|
|
72
|
+
def cache_enabled?
|
|
73
|
+
enabled?(:cache_enabled?) && cache_store.present?
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Returns the cache store
|
|
77
|
+
#
|
|
78
|
+
# @return [ActiveSupport::Cache::Store, nil]
|
|
79
|
+
def cache_store
|
|
80
|
+
global_config.cache_store
|
|
81
|
+
rescue StandardError
|
|
82
|
+
nil
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Returns the cache TTL
|
|
86
|
+
#
|
|
87
|
+
# @return [ActiveSupport::Duration, Integer, nil]
|
|
88
|
+
def cache_ttl
|
|
89
|
+
config(:cache_ttl)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Generates a cache key for the context
|
|
93
|
+
#
|
|
94
|
+
# The cache key includes:
|
|
95
|
+
# - Namespace prefix
|
|
96
|
+
# - Agent type
|
|
97
|
+
# - Agent class name
|
|
98
|
+
# - Version (for cache invalidation)
|
|
99
|
+
# - Model
|
|
100
|
+
# - SHA256 hash of input
|
|
101
|
+
#
|
|
102
|
+
# @param context [Context] The execution context
|
|
103
|
+
# @return [String] The cache key
|
|
104
|
+
def generate_cache_key(context)
|
|
105
|
+
components = [
|
|
106
|
+
"ruby_llm_agents",
|
|
107
|
+
context.agent_type,
|
|
108
|
+
context.agent_class&.name,
|
|
109
|
+
config(:version, "1.0"),
|
|
110
|
+
context.model,
|
|
111
|
+
hash_input(context.input)
|
|
112
|
+
].compact
|
|
113
|
+
|
|
114
|
+
components.join("/")
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# Hashes the input for cache key
|
|
118
|
+
#
|
|
119
|
+
# @param input [Object] The input to hash
|
|
120
|
+
# @return [String] SHA256 hash
|
|
121
|
+
def hash_input(input)
|
|
122
|
+
Digest::SHA256.hexdigest(serialize_input(input))
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# Serializes input for hashing
|
|
126
|
+
#
|
|
127
|
+
# @param input [Object] The input to serialize
|
|
128
|
+
# @return [String] Serialized representation
|
|
129
|
+
def serialize_input(input)
|
|
130
|
+
case input
|
|
131
|
+
when String
|
|
132
|
+
input
|
|
133
|
+
when Array
|
|
134
|
+
input.map { |i| serialize_input(i) }.join("|")
|
|
135
|
+
when Hash
|
|
136
|
+
input.sort.map { |k, v| "#{k}:#{serialize_input(v)}" }.join("|")
|
|
137
|
+
else
|
|
138
|
+
input.to_json
|
|
139
|
+
end
|
|
140
|
+
rescue StandardError
|
|
141
|
+
input.to_s
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
# Reads from cache
|
|
145
|
+
#
|
|
146
|
+
# @param key [String] Cache key
|
|
147
|
+
# @return [Object, nil] Cached value or nil
|
|
148
|
+
def cache_read(key)
|
|
149
|
+
cache_store.read(key)
|
|
150
|
+
rescue StandardError => e
|
|
151
|
+
error("Cache read failed: #{e.message}")
|
|
152
|
+
nil
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
# Writes to cache
|
|
156
|
+
#
|
|
157
|
+
# @param key [String] Cache key
|
|
158
|
+
# @param value [Object] Value to cache
|
|
159
|
+
def cache_write(key, value)
|
|
160
|
+
options = {}
|
|
161
|
+
options[:expires_in] = cache_ttl if cache_ttl
|
|
162
|
+
|
|
163
|
+
cache_store.write(key, value, **options)
|
|
164
|
+
rescue StandardError => e
|
|
165
|
+
error("Cache write failed: #{e.message}")
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
end
|