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,150 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
module Agents
|
|
5
|
+
# Base error class for RubyLLM::Agents
|
|
6
|
+
class Error < StandardError; end
|
|
7
|
+
|
|
8
|
+
# ============================================================
|
|
9
|
+
# Pipeline Errors
|
|
10
|
+
# ============================================================
|
|
11
|
+
|
|
12
|
+
# Base class for pipeline-related errors
|
|
13
|
+
class PipelineError < Error; end
|
|
14
|
+
|
|
15
|
+
# ============================================================
|
|
16
|
+
# Reliability Errors
|
|
17
|
+
# ============================================================
|
|
18
|
+
|
|
19
|
+
# Base class for reliability-related errors
|
|
20
|
+
class ReliabilityError < Error; end
|
|
21
|
+
|
|
22
|
+
# Raised when an error is retryable (transient)
|
|
23
|
+
class RetryableError < ReliabilityError; end
|
|
24
|
+
|
|
25
|
+
# Raised when a circuit breaker is open
|
|
26
|
+
class CircuitOpenError < ReliabilityError
|
|
27
|
+
# @return [String] The model that has an open circuit
|
|
28
|
+
attr_reader :model
|
|
29
|
+
|
|
30
|
+
def initialize(message = nil, model: nil)
|
|
31
|
+
@model = model
|
|
32
|
+
super(message || "Circuit breaker is open#{model ? " for #{model}" : ""}")
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Raised when total timeout is exceeded across all attempts
|
|
37
|
+
class TotalTimeoutError < ReliabilityError
|
|
38
|
+
# @return [Float] The timeout that was exceeded
|
|
39
|
+
attr_reader :timeout
|
|
40
|
+
|
|
41
|
+
# @return [Float] The elapsed time
|
|
42
|
+
attr_reader :elapsed
|
|
43
|
+
|
|
44
|
+
def initialize(message = nil, timeout: nil, elapsed: nil)
|
|
45
|
+
@timeout = timeout
|
|
46
|
+
@elapsed = elapsed
|
|
47
|
+
super(message || "Total timeout of #{timeout}s exceeded (elapsed: #{elapsed&.round(2)}s)")
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Raised when all models (primary + fallbacks) fail
|
|
52
|
+
class AllModelsFailedError < ReliabilityError
|
|
53
|
+
# @return [Array<Hash>] Details of each failed attempt
|
|
54
|
+
attr_reader :attempts
|
|
55
|
+
|
|
56
|
+
def initialize(message = nil, attempts: [])
|
|
57
|
+
@attempts = attempts
|
|
58
|
+
models = attempts.map { |a| a[:model] }.compact.join(", ")
|
|
59
|
+
super(message || "All models failed: #{models}")
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# ============================================================
|
|
64
|
+
# Budget Errors
|
|
65
|
+
# ============================================================
|
|
66
|
+
|
|
67
|
+
# Base class for budget-related errors
|
|
68
|
+
class BudgetError < Error; end
|
|
69
|
+
|
|
70
|
+
# Raised when budget is exceeded
|
|
71
|
+
class BudgetExceededError < BudgetError
|
|
72
|
+
# @return [String, nil] The tenant ID
|
|
73
|
+
attr_reader :tenant_id
|
|
74
|
+
|
|
75
|
+
# @return [String, nil] The budget type (daily, monthly, etc.)
|
|
76
|
+
attr_reader :budget_type
|
|
77
|
+
|
|
78
|
+
def initialize(message = nil, tenant_id: nil, budget_type: nil)
|
|
79
|
+
@tenant_id = tenant_id
|
|
80
|
+
@budget_type = budget_type
|
|
81
|
+
super(message || "Budget exceeded#{tenant_id ? " for tenant #{tenant_id}" : ""}")
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# ============================================================
|
|
86
|
+
# Configuration Errors
|
|
87
|
+
# ============================================================
|
|
88
|
+
|
|
89
|
+
# Raised for configuration issues
|
|
90
|
+
class ConfigurationError < Error; end
|
|
91
|
+
|
|
92
|
+
# Raised when content is flagged during moderation
|
|
93
|
+
#
|
|
94
|
+
# Contains the full moderation result and the phase where
|
|
95
|
+
# the content was flagged.
|
|
96
|
+
#
|
|
97
|
+
# @example Handling moderation errors
|
|
98
|
+
# begin
|
|
99
|
+
# result = MyAgent.call(message: user_input)
|
|
100
|
+
# rescue RubyLLM::Agents::ModerationError => e
|
|
101
|
+
# puts "Content blocked: #{e.flagged_categories.join(', ')}"
|
|
102
|
+
# puts "Phase: #{e.phase}"
|
|
103
|
+
# puts "Scores: #{e.category_scores}"
|
|
104
|
+
# end
|
|
105
|
+
#
|
|
106
|
+
# @api public
|
|
107
|
+
class ModerationError < Error
|
|
108
|
+
# @return [Object] The raw moderation result from RubyLLM
|
|
109
|
+
attr_reader :moderation_result
|
|
110
|
+
|
|
111
|
+
# @return [Symbol] The phase where content was flagged (:input or :output)
|
|
112
|
+
attr_reader :phase
|
|
113
|
+
|
|
114
|
+
# Creates a new ModerationError
|
|
115
|
+
#
|
|
116
|
+
# @param moderation_result [Object] The moderation result from RubyLLM
|
|
117
|
+
# @param phase [Symbol] The phase where content was flagged
|
|
118
|
+
def initialize(moderation_result, phase)
|
|
119
|
+
@moderation_result = moderation_result
|
|
120
|
+
@phase = phase
|
|
121
|
+
|
|
122
|
+
categories = moderation_result.flagged_categories
|
|
123
|
+
category_list = categories.respond_to?(:join) ? categories.join(", ") : categories.to_s
|
|
124
|
+
|
|
125
|
+
super("Content flagged during #{phase} moderation: #{category_list}")
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# Returns the flagged categories from the moderation result
|
|
129
|
+
#
|
|
130
|
+
# @return [Array<String, Symbol>] List of flagged categories
|
|
131
|
+
def flagged_categories
|
|
132
|
+
moderation_result.flagged_categories
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# Returns the category scores from the moderation result
|
|
136
|
+
#
|
|
137
|
+
# @return [Hash{String, Symbol => Float}] Category to score mapping
|
|
138
|
+
def category_scores
|
|
139
|
+
moderation_result.category_scores
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# Returns whether the moderation result was flagged
|
|
143
|
+
#
|
|
144
|
+
# @return [Boolean] Always true for ModerationError
|
|
145
|
+
def flagged?
|
|
146
|
+
true
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
end
|
|
@@ -245,7 +245,9 @@ module RubyLLM
|
|
|
245
245
|
metadata: metadata,
|
|
246
246
|
system_prompt: config.persist_prompts ? redacted_system_prompt : nil,
|
|
247
247
|
user_prompt: config.persist_prompts ? redacted_user_prompt : nil,
|
|
248
|
-
streaming: self.class.streaming
|
|
248
|
+
streaming: self.class.streaming,
|
|
249
|
+
messages_count: resolved_messages.size,
|
|
250
|
+
messages_summary: config.persist_messages_summary ? messages_summary : {}
|
|
249
251
|
}
|
|
250
252
|
|
|
251
253
|
# Extract tracing fields from metadata if present
|
|
@@ -326,6 +328,9 @@ module RubyLLM
|
|
|
326
328
|
Rails.logger.warn("[RubyLLM::Agents] Cost calculation failed: #{cost_error.message}")
|
|
327
329
|
end
|
|
328
330
|
end
|
|
331
|
+
|
|
332
|
+
# Record token usage for budget tracking
|
|
333
|
+
record_token_usage(execution)
|
|
329
334
|
rescue ActiveRecord::RecordInvalid => e
|
|
330
335
|
Rails.logger.error("[RubyLLM::Agents] Validation failed for execution #{execution&.id}: #{e.record.errors.full_messages.join(', ')}")
|
|
331
336
|
if Rails.env.development? || Rails.env.test?
|
|
@@ -415,6 +420,9 @@ module RubyLLM
|
|
|
415
420
|
Rails.logger.warn("[RubyLLM::Agents] Cost calculation failed: #{cost_error.message}")
|
|
416
421
|
end
|
|
417
422
|
end
|
|
423
|
+
|
|
424
|
+
# Record token usage for budget tracking
|
|
425
|
+
record_token_usage(execution)
|
|
418
426
|
rescue ActiveRecord::RecordInvalid => e
|
|
419
427
|
Rails.logger.error("[RubyLLM::Agents] Validation failed for execution #{execution&.id}: #{e.record.errors.full_messages.join(', ')}")
|
|
420
428
|
if Rails.env.development? || Rails.env.test?
|
|
@@ -440,6 +448,8 @@ module RubyLLM
|
|
|
440
448
|
# @param error [Exception, nil] The exception if failed
|
|
441
449
|
# @return [void]
|
|
442
450
|
def legacy_log_execution(completed_at:, status:, response: nil, error: nil)
|
|
451
|
+
config = RubyLLM::Agents.configuration
|
|
452
|
+
|
|
443
453
|
execution_data = {
|
|
444
454
|
agent_type: self.class.name,
|
|
445
455
|
agent_version: self.class.version,
|
|
@@ -452,7 +462,9 @@ module RubyLLM
|
|
|
452
462
|
parameters: sanitized_parameters,
|
|
453
463
|
metadata: execution_metadata,
|
|
454
464
|
system_prompt: safe_system_prompt,
|
|
455
|
-
user_prompt: safe_user_prompt
|
|
465
|
+
user_prompt: safe_user_prompt,
|
|
466
|
+
messages_count: resolved_messages.size,
|
|
467
|
+
messages_summary: config.persist_messages_summary ? messages_summary : {}
|
|
456
468
|
}
|
|
457
469
|
|
|
458
470
|
# Add response data if available (using safe extraction)
|
|
@@ -516,6 +528,38 @@ module RubyLLM
|
|
|
516
528
|
Redactor.redact_string(prompt)
|
|
517
529
|
end
|
|
518
530
|
|
|
531
|
+
# Returns a summary of messages (first and last, truncated)
|
|
532
|
+
#
|
|
533
|
+
# Creates a summary of the conversation messages containing the first
|
|
534
|
+
# and last messages (if different) with content truncated for storage.
|
|
535
|
+
#
|
|
536
|
+
# @return [Hash] Summary with :first and :last message hashes, or empty hash
|
|
537
|
+
def messages_summary
|
|
538
|
+
msgs = resolved_messages
|
|
539
|
+
return {} if msgs.blank?
|
|
540
|
+
|
|
541
|
+
max_len = RubyLLM::Agents.configuration.messages_summary_max_length || 500
|
|
542
|
+
|
|
543
|
+
summary = {}
|
|
544
|
+
|
|
545
|
+
if msgs.first
|
|
546
|
+
summary[:first] = {
|
|
547
|
+
role: msgs.first[:role].to_s,
|
|
548
|
+
content: Redactor.redact_string(msgs.first[:content].to_s).truncate(max_len)
|
|
549
|
+
}
|
|
550
|
+
end
|
|
551
|
+
|
|
552
|
+
# Only add last if there are multiple messages and last is different from first
|
|
553
|
+
if msgs.size > 1 && msgs.last
|
|
554
|
+
summary[:last] = {
|
|
555
|
+
role: msgs.last[:role].to_s,
|
|
556
|
+
content: Redactor.redact_string(msgs.last[:content].to_s).truncate(max_len)
|
|
557
|
+
}
|
|
558
|
+
end
|
|
559
|
+
|
|
560
|
+
summary
|
|
561
|
+
end
|
|
562
|
+
|
|
519
563
|
# Returns the response with redaction applied
|
|
520
564
|
#
|
|
521
565
|
# @param response [RubyLLM::Message] The LLM response
|
|
@@ -583,6 +627,9 @@ module RubyLLM
|
|
|
583
627
|
# during multi-turn conversations (when tools are used)
|
|
584
628
|
tool_calls_data = respond_to?(:accumulated_tool_calls) ? accumulated_tool_calls : []
|
|
585
629
|
|
|
630
|
+
# Extract thinking data if present
|
|
631
|
+
thinking_data = safe_extract_thinking_data(response)
|
|
632
|
+
|
|
586
633
|
{
|
|
587
634
|
input_tokens: safe_response_value(response, :input_tokens),
|
|
588
635
|
output_tokens: safe_response_value(response, :output_tokens),
|
|
@@ -593,7 +640,7 @@ module RubyLLM
|
|
|
593
640
|
response: safe_serialize_response(response),
|
|
594
641
|
tool_calls: tool_calls_data || [],
|
|
595
642
|
tool_calls_count: tool_calls_data&.size || 0
|
|
596
|
-
}.compact
|
|
643
|
+
}.merge(thinking_data).compact
|
|
597
644
|
end
|
|
598
645
|
|
|
599
646
|
# Extracts finish reason from response, normalizing to standard values
|
|
@@ -621,6 +668,24 @@ module RubyLLM
|
|
|
621
668
|
end
|
|
622
669
|
end
|
|
623
670
|
|
|
671
|
+
# Extracts thinking data from response
|
|
672
|
+
#
|
|
673
|
+
# Handles different response structures from various providers.
|
|
674
|
+
# The thinking object typically has text, signature, and tokens.
|
|
675
|
+
#
|
|
676
|
+
# @param response [RubyLLM::Message] The LLM response
|
|
677
|
+
# @return [Hash] Thinking data (empty if none present)
|
|
678
|
+
def safe_extract_thinking_data(response)
|
|
679
|
+
thinking = safe_response_value(response, :thinking)
|
|
680
|
+
return {} unless thinking
|
|
681
|
+
|
|
682
|
+
{
|
|
683
|
+
thinking_text: thinking.respond_to?(:text) ? thinking.text : thinking[:text],
|
|
684
|
+
thinking_signature: thinking.respond_to?(:signature) ? thinking.signature : thinking[:signature],
|
|
685
|
+
thinking_tokens: thinking.respond_to?(:tokens) ? thinking.tokens : thinking[:tokens]
|
|
686
|
+
}.compact
|
|
687
|
+
end
|
|
688
|
+
|
|
624
689
|
# Extracts routing/retry tracking data from attempt tracker
|
|
625
690
|
#
|
|
626
691
|
# Analyzes the execution attempts to determine:
|
|
@@ -773,7 +838,9 @@ module RubyLLM
|
|
|
773
838
|
total_cost: 0,
|
|
774
839
|
parameters: redacted_parameters,
|
|
775
840
|
metadata: execution_metadata,
|
|
776
|
-
streaming: self.class.streaming
|
|
841
|
+
streaming: self.class.streaming,
|
|
842
|
+
messages_count: resolved_messages.size,
|
|
843
|
+
messages_summary: config.persist_messages_summary ? messages_summary : {}
|
|
777
844
|
}
|
|
778
845
|
|
|
779
846
|
# Add tracing fields from metadata if present
|
|
@@ -798,6 +865,28 @@ module RubyLLM
|
|
|
798
865
|
Rails.logger.error("[RubyLLM::Agents] Failed to record cache hit execution: #{e.message}")
|
|
799
866
|
end
|
|
800
867
|
|
|
868
|
+
# Records token usage to the BudgetTracker
|
|
869
|
+
#
|
|
870
|
+
# @param execution [Execution] The completed execution record
|
|
871
|
+
# @return [void]
|
|
872
|
+
def record_token_usage(execution)
|
|
873
|
+
return unless execution&.total_tokens && execution.total_tokens > 0
|
|
874
|
+
|
|
875
|
+
begin
|
|
876
|
+
tenant_id = respond_to?(:resolved_tenant_id) ? resolved_tenant_id : nil
|
|
877
|
+
tenant_config = respond_to?(:runtime_tenant_config) ? runtime_tenant_config : nil
|
|
878
|
+
|
|
879
|
+
BudgetTracker.record_tokens!(
|
|
880
|
+
self.class.name,
|
|
881
|
+
execution.total_tokens,
|
|
882
|
+
tenant_id: tenant_id,
|
|
883
|
+
tenant_config: tenant_config
|
|
884
|
+
)
|
|
885
|
+
rescue StandardError => e
|
|
886
|
+
Rails.logger.warn("[RubyLLM::Agents] Failed to record token usage: #{e.message}")
|
|
887
|
+
end
|
|
888
|
+
end
|
|
889
|
+
|
|
801
890
|
# Emergency fallback to mark execution as failed
|
|
802
891
|
#
|
|
803
892
|
# Uses update_all to bypass ActiveRecord callbacks and validations,
|
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "active_support/concern"
|
|
4
|
+
|
|
5
|
+
module RubyLLM
|
|
6
|
+
module Agents
|
|
7
|
+
# DSL for declaring Rails models as LLM tenants
|
|
8
|
+
#
|
|
9
|
+
# Provides automatic budget management and usage tracking when included
|
|
10
|
+
# in ActiveRecord models. Models using this concern can be passed as
|
|
11
|
+
# the `tenant:` parameter to agents.
|
|
12
|
+
#
|
|
13
|
+
# @example Basic usage
|
|
14
|
+
# class Organization < ApplicationRecord
|
|
15
|
+
# llm_tenant
|
|
16
|
+
# end
|
|
17
|
+
#
|
|
18
|
+
# @example With custom ID method
|
|
19
|
+
# class Organization < ApplicationRecord
|
|
20
|
+
# llm_tenant id: :slug
|
|
21
|
+
# end
|
|
22
|
+
#
|
|
23
|
+
# @example With auto-created budget
|
|
24
|
+
# class Organization < ApplicationRecord
|
|
25
|
+
# llm_tenant id: :slug, budget: true
|
|
26
|
+
# end
|
|
27
|
+
#
|
|
28
|
+
# @example With limits (auto-creates budget)
|
|
29
|
+
# class Organization < ApplicationRecord
|
|
30
|
+
# llm_tenant(
|
|
31
|
+
# id: :slug,
|
|
32
|
+
# name: :company_name,
|
|
33
|
+
# limits: {
|
|
34
|
+
# daily_cost: 100,
|
|
35
|
+
# monthly_cost: 1000,
|
|
36
|
+
# daily_executions: 500
|
|
37
|
+
# },
|
|
38
|
+
# enforcement: :hard
|
|
39
|
+
# )
|
|
40
|
+
# end
|
|
41
|
+
#
|
|
42
|
+
# @example With API keys from model columns/methods
|
|
43
|
+
# class Organization < ApplicationRecord
|
|
44
|
+
# encrypts :openai_api_key, :anthropic_api_key # Rails 7+ encryption
|
|
45
|
+
#
|
|
46
|
+
# llm_tenant(
|
|
47
|
+
# id: :slug,
|
|
48
|
+
# api_keys: {
|
|
49
|
+
# openai: :openai_api_key, # column name
|
|
50
|
+
# anthropic: :anthropic_api_key, # column name
|
|
51
|
+
# gemini: :fetch_gemini_key # custom method
|
|
52
|
+
# }
|
|
53
|
+
# )
|
|
54
|
+
#
|
|
55
|
+
# def fetch_gemini_key
|
|
56
|
+
# Vault.read("secret/#{slug}/gemini")
|
|
57
|
+
# end
|
|
58
|
+
# end
|
|
59
|
+
#
|
|
60
|
+
# @see RubyLLM::Agents::TenantBudget
|
|
61
|
+
# @api public
|
|
62
|
+
module LLMTenant
|
|
63
|
+
extend ActiveSupport::Concern
|
|
64
|
+
|
|
65
|
+
included do
|
|
66
|
+
# Executions tracked for this tenant
|
|
67
|
+
has_many :llm_executions,
|
|
68
|
+
class_name: "RubyLLM::Agents::Execution",
|
|
69
|
+
as: :tenant_record,
|
|
70
|
+
dependent: :nullify
|
|
71
|
+
|
|
72
|
+
# Budget association (optional)
|
|
73
|
+
has_one :llm_budget,
|
|
74
|
+
class_name: "RubyLLM::Agents::TenantBudget",
|
|
75
|
+
as: :tenant_record,
|
|
76
|
+
dependent: :destroy
|
|
77
|
+
|
|
78
|
+
# Store options at class level
|
|
79
|
+
class_attribute :llm_tenant_options, default: {}
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
class_methods do
|
|
83
|
+
# Declares this model as an LLM tenant
|
|
84
|
+
#
|
|
85
|
+
# @param id [Symbol] Method to call for tenant_id string (default: :id)
|
|
86
|
+
# @param name [Symbol] Method for budget display name (default: :to_s)
|
|
87
|
+
# @param budget [Boolean] Auto-create TenantBudget on model creation (default: false)
|
|
88
|
+
# @param limits [Hash] Default budget limits (implies budget: true)
|
|
89
|
+
# @param enforcement [Symbol] Budget enforcement mode (:none, :soft, :hard)
|
|
90
|
+
# @param inherit_global [Boolean] Inherit from global config (default: true)
|
|
91
|
+
# @param api_keys [Hash] Provider API keys mapping (e.g., { openai: :openai_api_key })
|
|
92
|
+
# @return [void]
|
|
93
|
+
def llm_tenant(id: :id, name: :to_s, budget: false, limits: nil, enforcement: nil, inherit_global: true, api_keys: nil)
|
|
94
|
+
self.llm_tenant_options = {
|
|
95
|
+
id: id,
|
|
96
|
+
name: name,
|
|
97
|
+
budget: budget || limits.present?,
|
|
98
|
+
limits: normalize_limits(limits),
|
|
99
|
+
enforcement: enforcement,
|
|
100
|
+
inherit_global: inherit_global,
|
|
101
|
+
api_keys: api_keys
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
# Auto-create budget callback
|
|
105
|
+
after_create :create_default_llm_budget if llm_tenant_options[:budget]
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
private
|
|
109
|
+
|
|
110
|
+
# Normalizes the limits hash to internal column names
|
|
111
|
+
#
|
|
112
|
+
# @param limits [Hash, nil] User-provided limits
|
|
113
|
+
# @return [Hash] Normalized limits
|
|
114
|
+
def normalize_limits(limits)
|
|
115
|
+
return {} if limits.blank?
|
|
116
|
+
|
|
117
|
+
{
|
|
118
|
+
daily_cost: limits[:daily_cost],
|
|
119
|
+
monthly_cost: limits[:monthly_cost],
|
|
120
|
+
daily_tokens: limits[:daily_tokens],
|
|
121
|
+
monthly_tokens: limits[:monthly_tokens],
|
|
122
|
+
daily_executions: limits[:daily_executions],
|
|
123
|
+
monthly_executions: limits[:monthly_executions]
|
|
124
|
+
}.compact
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# Returns the tenant_id string for this model
|
|
129
|
+
#
|
|
130
|
+
# @return [String] The tenant identifier
|
|
131
|
+
def llm_tenant_id
|
|
132
|
+
id_method = self.class.llm_tenant_options[:id] || :id
|
|
133
|
+
send(id_method).to_s
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# Returns API keys resolved from the DSL configuration
|
|
137
|
+
#
|
|
138
|
+
# Maps provider names (e.g., :openai, :anthropic) to their resolved values
|
|
139
|
+
# by calling the configured method/column on this model instance.
|
|
140
|
+
#
|
|
141
|
+
# @return [Hash] Provider to API key mapping (e.g., { openai: "sk-..." })
|
|
142
|
+
# @example
|
|
143
|
+
# org.llm_api_keys
|
|
144
|
+
# # => { openai: "sk-abc123", anthropic: "sk-ant-xyz789" }
|
|
145
|
+
def llm_api_keys
|
|
146
|
+
api_keys_config = self.class.llm_tenant_options[:api_keys]
|
|
147
|
+
return {} if api_keys_config.blank?
|
|
148
|
+
|
|
149
|
+
api_keys_config.transform_values do |method_name|
|
|
150
|
+
value = send(method_name)
|
|
151
|
+
value.presence
|
|
152
|
+
end.compact
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
# Returns cost for a given period
|
|
156
|
+
#
|
|
157
|
+
# @param period [Symbol, Range, nil] Time period (:today, :this_month, etc.)
|
|
158
|
+
# @return [BigDecimal] Total cost
|
|
159
|
+
def llm_cost(period: nil)
|
|
160
|
+
scope = llm_executions
|
|
161
|
+
scope = apply_period_scope(scope, period) if period
|
|
162
|
+
scope.sum(:total_cost) || 0
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
# Returns cost for today
|
|
166
|
+
#
|
|
167
|
+
# @return [BigDecimal] Today's cost
|
|
168
|
+
def llm_cost_today
|
|
169
|
+
llm_cost(period: :today)
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
# Returns cost for this month
|
|
173
|
+
#
|
|
174
|
+
# @return [BigDecimal] This month's cost
|
|
175
|
+
def llm_cost_this_month
|
|
176
|
+
llm_cost(period: :this_month)
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
# Returns token count for a given period
|
|
180
|
+
#
|
|
181
|
+
# @param period [Symbol, Range, nil] Time period
|
|
182
|
+
# @return [Integer] Total tokens
|
|
183
|
+
def llm_tokens(period: nil)
|
|
184
|
+
scope = llm_executions
|
|
185
|
+
scope = apply_period_scope(scope, period) if period
|
|
186
|
+
scope.sum(:total_tokens) || 0
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
# Returns tokens for today
|
|
190
|
+
#
|
|
191
|
+
# @return [Integer] Today's tokens
|
|
192
|
+
def llm_tokens_today
|
|
193
|
+
llm_tokens(period: :today)
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
# Returns tokens for this month
|
|
197
|
+
#
|
|
198
|
+
# @return [Integer] This month's tokens
|
|
199
|
+
def llm_tokens_this_month
|
|
200
|
+
llm_tokens(period: :this_month)
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
# Returns execution count for a given period
|
|
204
|
+
#
|
|
205
|
+
# @param period [Symbol, Range, nil] Time period
|
|
206
|
+
# @return [Integer] Execution count
|
|
207
|
+
def llm_execution_count(period: nil)
|
|
208
|
+
scope = llm_executions
|
|
209
|
+
scope = apply_period_scope(scope, period) if period
|
|
210
|
+
scope.count
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
# Returns executions for today
|
|
214
|
+
#
|
|
215
|
+
# @return [Integer] Today's execution count
|
|
216
|
+
def llm_executions_today
|
|
217
|
+
llm_execution_count(period: :today)
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
# Returns executions for this month
|
|
221
|
+
#
|
|
222
|
+
# @return [Integer] This month's execution count
|
|
223
|
+
def llm_executions_this_month
|
|
224
|
+
llm_execution_count(period: :this_month)
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
# Returns a usage summary for a given period
|
|
228
|
+
#
|
|
229
|
+
# @param period [Symbol] Time period (default: :this_month)
|
|
230
|
+
# @return [Hash] Usage summary with cost, tokens, and executions
|
|
231
|
+
def llm_usage_summary(period: :this_month)
|
|
232
|
+
{
|
|
233
|
+
cost: llm_cost(period: period),
|
|
234
|
+
tokens: llm_tokens(period: period),
|
|
235
|
+
executions: llm_execution_count(period: period),
|
|
236
|
+
period: period
|
|
237
|
+
}
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
# Returns or builds the associated TenantBudget
|
|
241
|
+
#
|
|
242
|
+
# @return [TenantBudget] The budget record
|
|
243
|
+
def llm_budget
|
|
244
|
+
super || build_llm_budget(tenant_id: llm_tenant_id)
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
# Configure budget with a block
|
|
248
|
+
#
|
|
249
|
+
# @yield [budget] The budget to configure
|
|
250
|
+
# @return [TenantBudget] The saved budget
|
|
251
|
+
def llm_configure_budget
|
|
252
|
+
budget = llm_budget
|
|
253
|
+
yield(budget) if block_given?
|
|
254
|
+
budget.save!
|
|
255
|
+
budget
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
# Returns the budget status from BudgetTracker
|
|
259
|
+
#
|
|
260
|
+
# @return [Hash] Budget status
|
|
261
|
+
def llm_budget_status
|
|
262
|
+
BudgetTracker.status(tenant_id: llm_tenant_id)
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
# Checks if within budget for a given limit type
|
|
266
|
+
#
|
|
267
|
+
# @param type [Symbol] Limit type (:daily_cost, :monthly_cost, :daily_tokens, etc.)
|
|
268
|
+
# @return [Boolean] true if within budget
|
|
269
|
+
def llm_within_budget?(type: :daily_cost)
|
|
270
|
+
status = llm_budget_status
|
|
271
|
+
return true unless status[:enabled]
|
|
272
|
+
|
|
273
|
+
key = budget_status_key(type)
|
|
274
|
+
status.dig(key, :percentage_used).to_f < 100
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
# Returns remaining budget for a given limit type
|
|
278
|
+
#
|
|
279
|
+
# @param type [Symbol] Limit type
|
|
280
|
+
# @return [Numeric, nil] Remaining amount
|
|
281
|
+
def llm_remaining_budget(type: :daily_cost)
|
|
282
|
+
status = llm_budget_status
|
|
283
|
+
key = budget_status_key(type)
|
|
284
|
+
status.dig(key, :remaining)
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
# Raises an error if over budget
|
|
288
|
+
#
|
|
289
|
+
# @raise [BudgetExceededError] if budget is exceeded
|
|
290
|
+
# @return [void]
|
|
291
|
+
def llm_check_budget!
|
|
292
|
+
BudgetTracker.check_budget!(self.class.name, tenant_id: llm_tenant_id)
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
private
|
|
296
|
+
|
|
297
|
+
# Applies a period scope to an execution query
|
|
298
|
+
#
|
|
299
|
+
# @param scope [ActiveRecord::Relation] The query scope
|
|
300
|
+
# @param period [Symbol, Range] The period to filter by
|
|
301
|
+
# @return [ActiveRecord::Relation] Filtered scope
|
|
302
|
+
def apply_period_scope(scope, period)
|
|
303
|
+
case period
|
|
304
|
+
when :today then scope.where(created_at: Time.current.all_day)
|
|
305
|
+
when :yesterday then scope.where(created_at: 1.day.ago.all_day)
|
|
306
|
+
when :this_week then scope.where(created_at: Time.current.all_week)
|
|
307
|
+
when :this_month then scope.where(created_at: Time.current.all_month)
|
|
308
|
+
when Range then scope.where(created_at: period)
|
|
309
|
+
else scope
|
|
310
|
+
end
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
# Maps user-friendly type to budget status key
|
|
314
|
+
#
|
|
315
|
+
# @param type [Symbol] User-friendly type
|
|
316
|
+
# @return [Symbol] Status key
|
|
317
|
+
def budget_status_key(type)
|
|
318
|
+
case type
|
|
319
|
+
when :daily_cost then :global_daily
|
|
320
|
+
when :monthly_cost then :global_monthly
|
|
321
|
+
when :daily_tokens then :global_daily_tokens
|
|
322
|
+
when :monthly_tokens then :global_monthly_tokens
|
|
323
|
+
when :daily_executions then :global_daily_executions
|
|
324
|
+
when :monthly_executions then :global_monthly_executions
|
|
325
|
+
else :global_daily
|
|
326
|
+
end
|
|
327
|
+
end
|
|
328
|
+
|
|
329
|
+
# Creates the default budget on model creation
|
|
330
|
+
#
|
|
331
|
+
# @return [void]
|
|
332
|
+
def create_default_llm_budget
|
|
333
|
+
return if self.class.llm_tenant_options.blank?
|
|
334
|
+
return if llm_budget&.persisted?
|
|
335
|
+
|
|
336
|
+
options = self.class.llm_tenant_options
|
|
337
|
+
limits = options[:limits] || {}
|
|
338
|
+
name_method = options[:name] || :to_s
|
|
339
|
+
|
|
340
|
+
budget = build_llm_budget(
|
|
341
|
+
tenant_id: llm_tenant_id,
|
|
342
|
+
name: send(name_method).to_s,
|
|
343
|
+
daily_limit: limits[:daily_cost],
|
|
344
|
+
monthly_limit: limits[:monthly_cost],
|
|
345
|
+
daily_token_limit: limits[:daily_tokens],
|
|
346
|
+
monthly_token_limit: limits[:monthly_tokens],
|
|
347
|
+
daily_execution_limit: limits[:daily_executions],
|
|
348
|
+
monthly_execution_limit: limits[:monthly_executions],
|
|
349
|
+
enforcement: options[:enforcement]&.to_s || "soft",
|
|
350
|
+
inherit_global_defaults: options.fetch(:inherit_global, true)
|
|
351
|
+
)
|
|
352
|
+
|
|
353
|
+
budget.tenant_record = self
|
|
354
|
+
budget.save!
|
|
355
|
+
end
|
|
356
|
+
end
|
|
357
|
+
end
|
|
358
|
+
end
|