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,386 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
module Agents
|
|
5
|
+
# Database-backed API configuration for LLM providers
|
|
6
|
+
#
|
|
7
|
+
# Stores API keys (encrypted at rest) and configuration options that can be
|
|
8
|
+
# managed via the dashboard UI. Supports both global settings and per-tenant
|
|
9
|
+
# overrides.
|
|
10
|
+
#
|
|
11
|
+
# Resolution priority: per-tenant DB > global DB > config file (RubyLLM.configure)
|
|
12
|
+
#
|
|
13
|
+
# @!attribute [rw] scope_type
|
|
14
|
+
# @return [String] Either 'global' or 'tenant'
|
|
15
|
+
# @!attribute [rw] scope_id
|
|
16
|
+
# @return [String, nil] Tenant ID when scope_type='tenant'
|
|
17
|
+
#
|
|
18
|
+
# @example Setting global API keys
|
|
19
|
+
# config = ApiConfiguration.global
|
|
20
|
+
# config.update!(
|
|
21
|
+
# openai_api_key: "sk-...",
|
|
22
|
+
# anthropic_api_key: "sk-ant-..."
|
|
23
|
+
# )
|
|
24
|
+
#
|
|
25
|
+
# @example Setting tenant-specific configuration
|
|
26
|
+
# tenant_config = ApiConfiguration.for_tenant!("acme_corp")
|
|
27
|
+
# tenant_config.update!(
|
|
28
|
+
# anthropic_api_key: "sk-ant-tenant-specific...",
|
|
29
|
+
# default_model: "claude-sonnet-4-20250514"
|
|
30
|
+
# )
|
|
31
|
+
#
|
|
32
|
+
# @example Resolving configuration for a tenant
|
|
33
|
+
# resolved = ApiConfiguration.resolve(tenant_id: "acme_corp")
|
|
34
|
+
# resolved.apply_to_ruby_llm! # Apply to RubyLLM.configuration
|
|
35
|
+
#
|
|
36
|
+
# @see ResolvedConfig
|
|
37
|
+
# @api public
|
|
38
|
+
class ApiConfiguration < ::ActiveRecord::Base
|
|
39
|
+
self.table_name = "ruby_llm_agents_api_configurations"
|
|
40
|
+
|
|
41
|
+
# Valid scope types
|
|
42
|
+
SCOPE_TYPES = %w[global tenant].freeze
|
|
43
|
+
|
|
44
|
+
# All API key attributes that should be encrypted
|
|
45
|
+
API_KEY_ATTRIBUTES = %i[
|
|
46
|
+
openai_api_key
|
|
47
|
+
anthropic_api_key
|
|
48
|
+
gemini_api_key
|
|
49
|
+
deepseek_api_key
|
|
50
|
+
mistral_api_key
|
|
51
|
+
perplexity_api_key
|
|
52
|
+
openrouter_api_key
|
|
53
|
+
gpustack_api_key
|
|
54
|
+
xai_api_key
|
|
55
|
+
ollama_api_key
|
|
56
|
+
bedrock_api_key
|
|
57
|
+
bedrock_secret_key
|
|
58
|
+
bedrock_session_token
|
|
59
|
+
vertexai_credentials
|
|
60
|
+
].freeze
|
|
61
|
+
|
|
62
|
+
# All endpoint attributes
|
|
63
|
+
ENDPOINT_ATTRIBUTES = %i[
|
|
64
|
+
openai_api_base
|
|
65
|
+
gemini_api_base
|
|
66
|
+
ollama_api_base
|
|
67
|
+
gpustack_api_base
|
|
68
|
+
xai_api_base
|
|
69
|
+
].freeze
|
|
70
|
+
|
|
71
|
+
# All default model attributes
|
|
72
|
+
MODEL_ATTRIBUTES = %i[
|
|
73
|
+
default_model
|
|
74
|
+
default_embedding_model
|
|
75
|
+
default_image_model
|
|
76
|
+
default_moderation_model
|
|
77
|
+
].freeze
|
|
78
|
+
|
|
79
|
+
# Connection settings attributes
|
|
80
|
+
CONNECTION_ATTRIBUTES = %i[
|
|
81
|
+
request_timeout
|
|
82
|
+
max_retries
|
|
83
|
+
retry_interval
|
|
84
|
+
retry_backoff_factor
|
|
85
|
+
retry_interval_randomness
|
|
86
|
+
http_proxy
|
|
87
|
+
].freeze
|
|
88
|
+
|
|
89
|
+
# All configurable attributes (excluding API keys)
|
|
90
|
+
NON_KEY_ATTRIBUTES = (
|
|
91
|
+
ENDPOINT_ATTRIBUTES +
|
|
92
|
+
MODEL_ATTRIBUTES +
|
|
93
|
+
CONNECTION_ATTRIBUTES +
|
|
94
|
+
%i[
|
|
95
|
+
openai_organization_id
|
|
96
|
+
openai_project_id
|
|
97
|
+
bedrock_region
|
|
98
|
+
vertexai_project_id
|
|
99
|
+
vertexai_location
|
|
100
|
+
]
|
|
101
|
+
).freeze
|
|
102
|
+
|
|
103
|
+
# Encrypt all API keys using Rails encrypted attributes
|
|
104
|
+
# Requires Rails encryption to be configured (rails credentials:edit)
|
|
105
|
+
API_KEY_ATTRIBUTES.each do |key_attr|
|
|
106
|
+
encrypts key_attr, deterministic: false
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# Validations
|
|
110
|
+
validates :scope_type, presence: true, inclusion: { in: SCOPE_TYPES }
|
|
111
|
+
validates :scope_id, uniqueness: { scope: :scope_type }, allow_nil: true
|
|
112
|
+
validate :validate_scope_consistency
|
|
113
|
+
|
|
114
|
+
# Scopes
|
|
115
|
+
scope :global_config, -> { where(scope_type: "global", scope_id: nil) }
|
|
116
|
+
scope :for_scope, ->(type, id) { where(scope_type: type, scope_id: id) }
|
|
117
|
+
scope :tenant_configs, -> { where(scope_type: "tenant") }
|
|
118
|
+
|
|
119
|
+
# Provider configuration mappings for display
|
|
120
|
+
PROVIDERS = {
|
|
121
|
+
openai: {
|
|
122
|
+
name: "OpenAI",
|
|
123
|
+
key_attr: :openai_api_key,
|
|
124
|
+
base_attr: :openai_api_base,
|
|
125
|
+
extra_attrs: [:openai_organization_id, :openai_project_id],
|
|
126
|
+
capabilities: ["Chat", "Embeddings", "Images", "Moderation"]
|
|
127
|
+
},
|
|
128
|
+
anthropic: {
|
|
129
|
+
name: "Anthropic",
|
|
130
|
+
key_attr: :anthropic_api_key,
|
|
131
|
+
capabilities: ["Chat"]
|
|
132
|
+
},
|
|
133
|
+
gemini: {
|
|
134
|
+
name: "Google Gemini",
|
|
135
|
+
key_attr: :gemini_api_key,
|
|
136
|
+
base_attr: :gemini_api_base,
|
|
137
|
+
capabilities: ["Chat", "Embeddings", "Images"]
|
|
138
|
+
},
|
|
139
|
+
deepseek: {
|
|
140
|
+
name: "DeepSeek",
|
|
141
|
+
key_attr: :deepseek_api_key,
|
|
142
|
+
capabilities: ["Chat"]
|
|
143
|
+
},
|
|
144
|
+
mistral: {
|
|
145
|
+
name: "Mistral",
|
|
146
|
+
key_attr: :mistral_api_key,
|
|
147
|
+
capabilities: ["Chat", "Embeddings"]
|
|
148
|
+
},
|
|
149
|
+
perplexity: {
|
|
150
|
+
name: "Perplexity",
|
|
151
|
+
key_attr: :perplexity_api_key,
|
|
152
|
+
capabilities: ["Chat"]
|
|
153
|
+
},
|
|
154
|
+
openrouter: {
|
|
155
|
+
name: "OpenRouter",
|
|
156
|
+
key_attr: :openrouter_api_key,
|
|
157
|
+
capabilities: ["Chat"]
|
|
158
|
+
},
|
|
159
|
+
gpustack: {
|
|
160
|
+
name: "GPUStack",
|
|
161
|
+
key_attr: :gpustack_api_key,
|
|
162
|
+
base_attr: :gpustack_api_base,
|
|
163
|
+
capabilities: ["Chat"]
|
|
164
|
+
},
|
|
165
|
+
xai: {
|
|
166
|
+
name: "xAI",
|
|
167
|
+
key_attr: :xai_api_key,
|
|
168
|
+
base_attr: :xai_api_base,
|
|
169
|
+
capabilities: ["Chat"]
|
|
170
|
+
},
|
|
171
|
+
ollama: {
|
|
172
|
+
name: "Ollama",
|
|
173
|
+
key_attr: :ollama_api_key,
|
|
174
|
+
base_attr: :ollama_api_base,
|
|
175
|
+
capabilities: ["Chat", "Embeddings"]
|
|
176
|
+
},
|
|
177
|
+
bedrock: {
|
|
178
|
+
name: "AWS Bedrock",
|
|
179
|
+
key_attr: :bedrock_api_key,
|
|
180
|
+
extra_attrs: [:bedrock_secret_key, :bedrock_session_token, :bedrock_region],
|
|
181
|
+
capabilities: ["Chat", "Embeddings"]
|
|
182
|
+
},
|
|
183
|
+
vertexai: {
|
|
184
|
+
name: "Google Vertex AI",
|
|
185
|
+
key_attr: :vertexai_credentials,
|
|
186
|
+
extra_attrs: [:vertexai_project_id, :vertexai_location],
|
|
187
|
+
capabilities: ["Chat", "Embeddings"]
|
|
188
|
+
}
|
|
189
|
+
}.freeze
|
|
190
|
+
|
|
191
|
+
class << self
|
|
192
|
+
# Finds or creates the global configuration
|
|
193
|
+
#
|
|
194
|
+
# @return [ApiConfiguration] The global configuration record
|
|
195
|
+
def global
|
|
196
|
+
global_config.first_or_create!
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
# Finds a tenant-specific configuration
|
|
200
|
+
#
|
|
201
|
+
# @param tenant_id [String] The tenant identifier
|
|
202
|
+
# @return [ApiConfiguration, nil] The tenant configuration or nil
|
|
203
|
+
def for_tenant(tenant_id)
|
|
204
|
+
return nil if tenant_id.blank?
|
|
205
|
+
|
|
206
|
+
for_scope("tenant", tenant_id).first
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
# Finds or creates a tenant-specific configuration
|
|
210
|
+
#
|
|
211
|
+
# @param tenant_id [String] The tenant identifier
|
|
212
|
+
# @return [ApiConfiguration] The tenant configuration record
|
|
213
|
+
def for_tenant!(tenant_id)
|
|
214
|
+
raise ArgumentError, "tenant_id cannot be blank" if tenant_id.blank?
|
|
215
|
+
|
|
216
|
+
for_scope("tenant", tenant_id).first_or_create!(
|
|
217
|
+
scope_type: "tenant",
|
|
218
|
+
scope_id: tenant_id
|
|
219
|
+
)
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
# Resolves the effective configuration for a given tenant
|
|
223
|
+
#
|
|
224
|
+
# Creates a ResolvedConfig that combines tenant config > global DB > RubyLLM config
|
|
225
|
+
#
|
|
226
|
+
# @param tenant_id [String, nil] Optional tenant identifier
|
|
227
|
+
# @return [ResolvedConfig] The resolved configuration
|
|
228
|
+
def resolve(tenant_id: nil)
|
|
229
|
+
tenant_config = tenant_id.present? ? for_tenant(tenant_id) : nil
|
|
230
|
+
global = global_config.first
|
|
231
|
+
|
|
232
|
+
RubyLLM::Agents::ResolvedConfig.new(
|
|
233
|
+
tenant_config: tenant_config,
|
|
234
|
+
global_config: global,
|
|
235
|
+
ruby_llm_config: ruby_llm_current_config
|
|
236
|
+
)
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
# Attempts to get the current RubyLLM configuration object
|
|
240
|
+
# Gets the current RubyLLM configuration object
|
|
241
|
+
#
|
|
242
|
+
# @return [Object, nil] The RubyLLM config object or nil
|
|
243
|
+
def ruby_llm_current_config
|
|
244
|
+
return nil unless defined?(::RubyLLM)
|
|
245
|
+
return nil unless RubyLLM.respond_to?(:config)
|
|
246
|
+
|
|
247
|
+
RubyLLM.config
|
|
248
|
+
rescue StandardError
|
|
249
|
+
nil
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
# Checks if the table exists (for graceful degradation)
|
|
253
|
+
#
|
|
254
|
+
# @return [Boolean]
|
|
255
|
+
def table_exists?
|
|
256
|
+
connection.table_exists?(table_name)
|
|
257
|
+
rescue ActiveRecord::NoDatabaseError, ActiveRecord::StatementInvalid
|
|
258
|
+
false
|
|
259
|
+
end
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
# Checks if a specific attribute has a value set
|
|
263
|
+
#
|
|
264
|
+
# @param attr_name [Symbol, String] The attribute name
|
|
265
|
+
# @return [Boolean]
|
|
266
|
+
def has_value?(attr_name)
|
|
267
|
+
value = send(attr_name)
|
|
268
|
+
value.present?
|
|
269
|
+
rescue NoMethodError
|
|
270
|
+
false
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
# Returns a masked version of an API key for display
|
|
274
|
+
#
|
|
275
|
+
# @param attr_name [Symbol, String] The API key attribute name
|
|
276
|
+
# @return [String, nil] Masked key like "sk-ab****wxyz" or nil
|
|
277
|
+
def masked_key(attr_name)
|
|
278
|
+
value = send(attr_name)
|
|
279
|
+
return nil if value.blank?
|
|
280
|
+
|
|
281
|
+
mask_string(value)
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
# Returns the source of this configuration
|
|
285
|
+
#
|
|
286
|
+
# @return [String] "global" or "tenant:ID"
|
|
287
|
+
def source_label
|
|
288
|
+
scope_type == "global" ? "Global" : "Tenant: #{scope_id}"
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
# Converts this configuration to a hash suitable for RubyLLM
|
|
292
|
+
#
|
|
293
|
+
# @return [Hash] Configuration hash with non-nil values
|
|
294
|
+
def to_ruby_llm_config
|
|
295
|
+
{}.tap do |config|
|
|
296
|
+
# API keys
|
|
297
|
+
config[:openai_api_key] = openai_api_key if openai_api_key.present?
|
|
298
|
+
config[:anthropic_api_key] = anthropic_api_key if anthropic_api_key.present?
|
|
299
|
+
config[:gemini_api_key] = gemini_api_key if gemini_api_key.present?
|
|
300
|
+
config[:deepseek_api_key] = deepseek_api_key if deepseek_api_key.present?
|
|
301
|
+
config[:mistral_api_key] = mistral_api_key if mistral_api_key.present?
|
|
302
|
+
config[:perplexity_api_key] = perplexity_api_key if perplexity_api_key.present?
|
|
303
|
+
config[:openrouter_api_key] = openrouter_api_key if openrouter_api_key.present?
|
|
304
|
+
config[:gpustack_api_key] = gpustack_api_key if gpustack_api_key.present?
|
|
305
|
+
config[:xai_api_key] = xai_api_key if xai_api_key.present?
|
|
306
|
+
config[:ollama_api_key] = ollama_api_key if ollama_api_key.present?
|
|
307
|
+
|
|
308
|
+
# Bedrock
|
|
309
|
+
config[:bedrock_api_key] = bedrock_api_key if bedrock_api_key.present?
|
|
310
|
+
config[:bedrock_secret_key] = bedrock_secret_key if bedrock_secret_key.present?
|
|
311
|
+
config[:bedrock_session_token] = bedrock_session_token if bedrock_session_token.present?
|
|
312
|
+
config[:bedrock_region] = bedrock_region if bedrock_region.present?
|
|
313
|
+
|
|
314
|
+
# Vertex AI
|
|
315
|
+
config[:vertexai_credentials] = vertexai_credentials if vertexai_credentials.present?
|
|
316
|
+
config[:vertexai_project_id] = vertexai_project_id if vertexai_project_id.present?
|
|
317
|
+
config[:vertexai_location] = vertexai_location if vertexai_location.present?
|
|
318
|
+
|
|
319
|
+
# Endpoints
|
|
320
|
+
config[:openai_api_base] = openai_api_base if openai_api_base.present?
|
|
321
|
+
config[:gemini_api_base] = gemini_api_base if gemini_api_base.present?
|
|
322
|
+
config[:ollama_api_base] = ollama_api_base if ollama_api_base.present?
|
|
323
|
+
config[:gpustack_api_base] = gpustack_api_base if gpustack_api_base.present?
|
|
324
|
+
config[:xai_api_base] = xai_api_base if xai_api_base.present?
|
|
325
|
+
|
|
326
|
+
# OpenAI options
|
|
327
|
+
config[:openai_organization_id] = openai_organization_id if openai_organization_id.present?
|
|
328
|
+
config[:openai_project_id] = openai_project_id if openai_project_id.present?
|
|
329
|
+
|
|
330
|
+
# Default models
|
|
331
|
+
config[:default_model] = default_model if default_model.present?
|
|
332
|
+
config[:default_embedding_model] = default_embedding_model if default_embedding_model.present?
|
|
333
|
+
config[:default_image_model] = default_image_model if default_image_model.present?
|
|
334
|
+
config[:default_moderation_model] = default_moderation_model if default_moderation_model.present?
|
|
335
|
+
|
|
336
|
+
# Connection settings
|
|
337
|
+
config[:request_timeout] = request_timeout if request_timeout.present?
|
|
338
|
+
config[:max_retries] = max_retries if max_retries.present?
|
|
339
|
+
config[:retry_interval] = retry_interval if retry_interval.present?
|
|
340
|
+
config[:retry_backoff_factor] = retry_backoff_factor if retry_backoff_factor.present?
|
|
341
|
+
config[:retry_interval_randomness] = retry_interval_randomness if retry_interval_randomness.present?
|
|
342
|
+
config[:http_proxy] = http_proxy if http_proxy.present?
|
|
343
|
+
end
|
|
344
|
+
end
|
|
345
|
+
|
|
346
|
+
# Returns provider status information for display
|
|
347
|
+
#
|
|
348
|
+
# @return [Array<Hash>] Array of provider status hashes
|
|
349
|
+
def provider_statuses
|
|
350
|
+
PROVIDERS.map do |key, info|
|
|
351
|
+
key_value = send(info[:key_attr])
|
|
352
|
+
{
|
|
353
|
+
key: key,
|
|
354
|
+
name: info[:name],
|
|
355
|
+
configured: key_value.present?,
|
|
356
|
+
masked_key: key_value.present? ? mask_string(key_value) : nil,
|
|
357
|
+
capabilities: info[:capabilities],
|
|
358
|
+
has_base_url: info[:base_attr].present? && send(info[:base_attr]).present?
|
|
359
|
+
}
|
|
360
|
+
end
|
|
361
|
+
end
|
|
362
|
+
|
|
363
|
+
private
|
|
364
|
+
|
|
365
|
+
# Validates scope consistency
|
|
366
|
+
def validate_scope_consistency
|
|
367
|
+
if scope_type == "global" && scope_id.present?
|
|
368
|
+
errors.add(:scope_id, "must be nil for global scope")
|
|
369
|
+
elsif scope_type == "tenant" && scope_id.blank?
|
|
370
|
+
errors.add(:scope_id, "must be present for tenant scope")
|
|
371
|
+
end
|
|
372
|
+
end
|
|
373
|
+
|
|
374
|
+
# Masks a string for display (shows first 2 and last 4 chars)
|
|
375
|
+
#
|
|
376
|
+
# @param value [String] The string to mask
|
|
377
|
+
# @return [String] Masked string
|
|
378
|
+
def mask_string(value)
|
|
379
|
+
return nil if value.blank?
|
|
380
|
+
return "****" if value.length <= 8
|
|
381
|
+
|
|
382
|
+
"#{value[0..1]}****#{value[-4..]}"
|
|
383
|
+
end
|
|
384
|
+
end
|
|
385
|
+
end
|
|
386
|
+
end
|
|
@@ -72,6 +72,9 @@ module RubyLLM
|
|
|
72
72
|
has_many :child_executions, class_name: "RubyLLM::Agents::Execution",
|
|
73
73
|
foreign_key: :parent_execution_id, dependent: :nullify, inverse_of: :parent_execution
|
|
74
74
|
|
|
75
|
+
# Polymorphic association to tenant model (for llm_tenant DSL)
|
|
76
|
+
belongs_to :tenant_record, polymorphic: true, optional: true
|
|
77
|
+
|
|
75
78
|
# Validations
|
|
76
79
|
validates :agent_type, :model_id, :started_at, presence: true
|
|
77
80
|
validates :status, inclusion: { in: statuses.keys }
|
|
@@ -6,36 +6,56 @@ module RubyLLM
|
|
|
6
6
|
#
|
|
7
7
|
# Stores per-tenant budget limits that override the global configuration.
|
|
8
8
|
# Supports runtime updates without application restarts.
|
|
9
|
+
# Supports cost-based (USD), token-based, and execution-based limits.
|
|
9
10
|
#
|
|
10
11
|
# @!attribute [rw] tenant_id
|
|
11
12
|
# @return [String] Unique identifier for the tenant
|
|
13
|
+
# @!attribute [rw] name
|
|
14
|
+
# @return [String, nil] Human-readable name for the tenant
|
|
12
15
|
# @!attribute [rw] daily_limit
|
|
13
16
|
# @return [BigDecimal, nil] Daily budget limit in USD
|
|
14
17
|
# @!attribute [rw] monthly_limit
|
|
15
18
|
# @return [BigDecimal, nil] Monthly budget limit in USD
|
|
19
|
+
# @!attribute [rw] daily_token_limit
|
|
20
|
+
# @return [Integer, nil] Daily token limit (across all models)
|
|
21
|
+
# @!attribute [rw] monthly_token_limit
|
|
22
|
+
# @return [Integer, nil] Monthly token limit (across all models)
|
|
23
|
+
# @!attribute [rw] daily_execution_limit
|
|
24
|
+
# @return [Integer, nil] Daily execution/call limit
|
|
25
|
+
# @!attribute [rw] monthly_execution_limit
|
|
26
|
+
# @return [Integer, nil] Monthly execution/call limit
|
|
16
27
|
# @!attribute [rw] per_agent_daily
|
|
17
|
-
# @return [Hash] Per-agent daily limits: { "AgentName" => limit }
|
|
28
|
+
# @return [Hash] Per-agent daily cost limits: { "AgentName" => limit }
|
|
18
29
|
# @!attribute [rw] per_agent_monthly
|
|
19
|
-
# @return [Hash] Per-agent monthly limits: { "AgentName" => limit }
|
|
30
|
+
# @return [Hash] Per-agent monthly cost limits: { "AgentName" => limit }
|
|
20
31
|
# @!attribute [rw] enforcement
|
|
21
32
|
# @return [String] Enforcement mode: "none", "soft", or "hard"
|
|
22
33
|
# @!attribute [rw] inherit_global_defaults
|
|
23
34
|
# @return [Boolean] Whether to fall back to global config for unset limits
|
|
35
|
+
# @!attribute [rw] tenant_record
|
|
36
|
+
# @return [ActiveRecord::Base, nil] Polymorphic association to tenant model
|
|
24
37
|
#
|
|
25
|
-
# @example Creating a tenant budget
|
|
38
|
+
# @example Creating a tenant budget with cost, token, and execution limits
|
|
26
39
|
# TenantBudget.create!(
|
|
27
40
|
# tenant_id: "acme_corp",
|
|
28
|
-
#
|
|
29
|
-
#
|
|
30
|
-
#
|
|
41
|
+
# name: "Acme Corporation",
|
|
42
|
+
# daily_limit: 50.0, # USD
|
|
43
|
+
# monthly_limit: 500.0, # USD
|
|
44
|
+
# daily_token_limit: 1_000_000,
|
|
45
|
+
# monthly_token_limit: 10_000_000,
|
|
46
|
+
# daily_execution_limit: 500,
|
|
47
|
+
# monthly_execution_limit: 10_000,
|
|
31
48
|
# enforcement: "hard"
|
|
32
49
|
# )
|
|
33
50
|
#
|
|
34
|
-
# @example Fetching budget for a tenant
|
|
35
|
-
# budget = TenantBudget.for_tenant(
|
|
36
|
-
# budget.effective_daily_limit
|
|
51
|
+
# @example Fetching budget for a tenant object
|
|
52
|
+
# budget = TenantBudget.for_tenant(organization)
|
|
53
|
+
# budget.effective_daily_limit # => 50.0 (cost)
|
|
54
|
+
# budget.effective_daily_token_limit # => 1_000_000 (tokens)
|
|
55
|
+
# budget.effective_daily_execution_limit # => 500 (executions)
|
|
37
56
|
#
|
|
38
57
|
# @see RubyLLM::Agents::BudgetTracker
|
|
58
|
+
# @see RubyLLM::Agents::LLMTenant
|
|
39
59
|
# @api public
|
|
40
60
|
class TenantBudget < ::ActiveRecord::Base
|
|
41
61
|
self.table_name = "ruby_llm_agents_tenant_budgets"
|
|
@@ -43,20 +63,51 @@ module RubyLLM
|
|
|
43
63
|
# Valid enforcement modes
|
|
44
64
|
ENFORCEMENT_MODES = %w[none soft hard].freeze
|
|
45
65
|
|
|
66
|
+
# Polymorphic association to the tenant model (e.g., Organization, Account)
|
|
67
|
+
belongs_to :tenant_record, polymorphic: true, optional: true
|
|
68
|
+
|
|
46
69
|
# Validations
|
|
47
70
|
validates :tenant_id, presence: true, uniqueness: true
|
|
48
71
|
validates :enforcement, inclusion: { in: ENFORCEMENT_MODES }, allow_nil: true
|
|
49
72
|
validates :daily_limit, :monthly_limit,
|
|
50
73
|
numericality: { greater_than_or_equal_to: 0 }, allow_nil: true
|
|
74
|
+
validates :daily_token_limit, :monthly_token_limit,
|
|
75
|
+
numericality: { greater_than_or_equal_to: 0, only_integer: true }, allow_nil: true
|
|
76
|
+
validates :daily_execution_limit, :monthly_execution_limit,
|
|
77
|
+
numericality: { greater_than_or_equal_to: 0, only_integer: true }, allow_nil: true
|
|
51
78
|
|
|
52
79
|
# Finds a budget for the given tenant
|
|
53
80
|
#
|
|
54
|
-
# @param
|
|
81
|
+
# @param tenant [String, Object] The tenant identifier string or object with llm_tenant_id
|
|
55
82
|
# @return [TenantBudget, nil] The budget record or nil if not found
|
|
56
|
-
def self.for_tenant(
|
|
57
|
-
return nil if
|
|
83
|
+
def self.for_tenant(tenant)
|
|
84
|
+
return nil if tenant.blank?
|
|
85
|
+
|
|
86
|
+
if tenant.respond_to?(:llm_tenant_id)
|
|
87
|
+
# Object with llm_tenant DSL - try polymorphic first, then tenant_id
|
|
88
|
+
find_by(tenant_record: tenant) || find_by(tenant_id: tenant.llm_tenant_id)
|
|
89
|
+
else
|
|
90
|
+
# String tenant_id
|
|
91
|
+
find_by(tenant_id: tenant.to_s)
|
|
92
|
+
end
|
|
93
|
+
end
|
|
58
94
|
|
|
59
|
-
|
|
95
|
+
# Finds or creates a budget for the given tenant
|
|
96
|
+
#
|
|
97
|
+
# @param tenant_id [String] The tenant identifier
|
|
98
|
+
# @param name [String, nil] Optional human-readable name
|
|
99
|
+
# @return [TenantBudget] The budget record
|
|
100
|
+
def self.for_tenant!(tenant_id, name: nil)
|
|
101
|
+
find_or_create_by!(tenant_id: tenant_id) do |budget|
|
|
102
|
+
budget.name = name
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Returns the display name (name or tenant_id fallback)
|
|
107
|
+
#
|
|
108
|
+
# @return [String] The name to display
|
|
109
|
+
def display_name
|
|
110
|
+
name.presence || tenant_id
|
|
60
111
|
end
|
|
61
112
|
|
|
62
113
|
# Returns the effective daily limit, considering inheritance
|
|
@@ -103,6 +154,46 @@ module RubyLLM
|
|
|
103
154
|
global_config&.dig(:per_agent_monthly, agent_type)
|
|
104
155
|
end
|
|
105
156
|
|
|
157
|
+
# Returns the effective daily token limit, considering inheritance
|
|
158
|
+
#
|
|
159
|
+
# @return [Integer, nil] The daily token limit or nil if not set
|
|
160
|
+
def effective_daily_token_limit
|
|
161
|
+
return daily_token_limit if daily_token_limit.present?
|
|
162
|
+
return nil unless inherit_global_defaults
|
|
163
|
+
|
|
164
|
+
global_config&.dig(:global_daily_tokens)
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
# Returns the effective monthly token limit, considering inheritance
|
|
168
|
+
#
|
|
169
|
+
# @return [Integer, nil] The monthly token limit or nil if not set
|
|
170
|
+
def effective_monthly_token_limit
|
|
171
|
+
return monthly_token_limit if monthly_token_limit.present?
|
|
172
|
+
return nil unless inherit_global_defaults
|
|
173
|
+
|
|
174
|
+
global_config&.dig(:global_monthly_tokens)
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
# Returns the effective daily execution limit, considering inheritance
|
|
178
|
+
#
|
|
179
|
+
# @return [Integer, nil] The daily execution limit or nil if not set
|
|
180
|
+
def effective_daily_execution_limit
|
|
181
|
+
return daily_execution_limit if daily_execution_limit.present?
|
|
182
|
+
return nil unless inherit_global_defaults
|
|
183
|
+
|
|
184
|
+
global_config&.dig(:global_daily_executions)
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
# Returns the effective monthly execution limit, considering inheritance
|
|
188
|
+
#
|
|
189
|
+
# @return [Integer, nil] The monthly execution limit or nil if not set
|
|
190
|
+
def effective_monthly_execution_limit
|
|
191
|
+
return monthly_execution_limit if monthly_execution_limit.present?
|
|
192
|
+
return nil unless inherit_global_defaults
|
|
193
|
+
|
|
194
|
+
global_config&.dig(:global_monthly_executions)
|
|
195
|
+
end
|
|
196
|
+
|
|
106
197
|
# Returns the effective enforcement mode
|
|
107
198
|
#
|
|
108
199
|
# @return [Symbol] :none, :soft, or :hard
|
|
@@ -127,10 +218,17 @@ module RubyLLM
|
|
|
127
218
|
{
|
|
128
219
|
enabled: budgets_enabled?,
|
|
129
220
|
enforcement: effective_enforcement,
|
|
221
|
+
# Cost limits
|
|
130
222
|
global_daily: effective_daily_limit,
|
|
131
223
|
global_monthly: effective_monthly_limit,
|
|
132
224
|
per_agent_daily: merged_per_agent_daily,
|
|
133
|
-
per_agent_monthly: merged_per_agent_monthly
|
|
225
|
+
per_agent_monthly: merged_per_agent_monthly,
|
|
226
|
+
# Token limits
|
|
227
|
+
global_daily_tokens: effective_daily_token_limit,
|
|
228
|
+
global_monthly_tokens: effective_monthly_token_limit,
|
|
229
|
+
# Execution limits
|
|
230
|
+
global_daily_executions: effective_daily_execution_limit,
|
|
231
|
+
global_monthly_executions: effective_monthly_execution_limit
|
|
134
232
|
}
|
|
135
233
|
end
|
|
136
234
|
|
|
@@ -60,12 +60,19 @@ module RubyLLM
|
|
|
60
60
|
#
|
|
61
61
|
# @return [Array<String>] Agent class names
|
|
62
62
|
def file_system_agents
|
|
63
|
-
# Ensure all agent classes are loaded
|
|
63
|
+
# Ensure all agent and workflow classes are loaded
|
|
64
64
|
eager_load_agents!
|
|
65
65
|
|
|
66
|
-
# Find all descendants of
|
|
67
|
-
|
|
68
|
-
|
|
66
|
+
# Find all descendants of all base classes
|
|
67
|
+
agents = RubyLLM::Agents::Base.descendants.map(&:name).compact
|
|
68
|
+
workflows = RubyLLM::Agents::Workflow.descendants.map(&:name).compact
|
|
69
|
+
embedders = RubyLLM::Agents::Embedder.descendants.map(&:name).compact
|
|
70
|
+
moderators = RubyLLM::Agents::Moderator.descendants.map(&:name).compact
|
|
71
|
+
speakers = RubyLLM::Agents::Speaker.descendants.map(&:name).compact
|
|
72
|
+
transcribers = RubyLLM::Agents::Transcriber.descendants.map(&:name).compact
|
|
73
|
+
image_generators = RubyLLM::Agents::ImageGenerator.descendants.map(&:name).compact
|
|
74
|
+
|
|
75
|
+
(agents + workflows + embedders + moderators + speakers + transcribers + image_generators).uniq
|
|
69
76
|
rescue StandardError => e
|
|
70
77
|
Rails.logger.error("[RubyLLM::Agents] Error loading agents from file system: #{e.message}")
|
|
71
78
|
[]
|
|
@@ -81,17 +88,19 @@ module RubyLLM
|
|
|
81
88
|
[]
|
|
82
89
|
end
|
|
83
90
|
|
|
84
|
-
# Eager loads all agent files to register descendants
|
|
91
|
+
# Eager loads all agent and workflow files to register descendants
|
|
85
92
|
#
|
|
86
93
|
# @return [void]
|
|
87
94
|
def eager_load_agents!
|
|
88
|
-
|
|
89
|
-
|
|
95
|
+
%w[agents workflows embedders moderators speakers transcribers image_generators].each do |dir|
|
|
96
|
+
path = Rails.root.join("app", dir)
|
|
97
|
+
next unless path.exist?
|
|
90
98
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
99
|
+
Dir.glob(path.join("**", "*.rb")).each do |file|
|
|
100
|
+
require_dependency file
|
|
101
|
+
rescue LoadError, StandardError => e
|
|
102
|
+
Rails.logger.error("[RubyLLM::Agents] Failed to load file #{file}: #{e.message}")
|
|
103
|
+
end
|
|
95
104
|
end
|
|
96
105
|
end
|
|
97
106
|
|
|
@@ -125,8 +134,11 @@ module RubyLLM
|
|
|
125
134
|
agent_class = find(agent_type)
|
|
126
135
|
stats = fetch_stats(agent_type)
|
|
127
136
|
|
|
137
|
+
# Detect the agent type (agent, workflow, embedder, moderator, speaker, transcriber)
|
|
138
|
+
detected_type = detect_agent_type(agent_class)
|
|
139
|
+
|
|
128
140
|
# Check if this is a workflow class vs a regular agent
|
|
129
|
-
is_workflow =
|
|
141
|
+
is_workflow = detected_type == "workflow"
|
|
130
142
|
|
|
131
143
|
# Determine specific workflow type and children
|
|
132
144
|
workflow_type = is_workflow ? detect_workflow_type(agent_class) : nil
|
|
@@ -136,6 +148,7 @@ module RubyLLM
|
|
|
136
148
|
name: agent_type,
|
|
137
149
|
class: agent_class,
|
|
138
150
|
active: agent_class.present?,
|
|
151
|
+
agent_type: detected_type,
|
|
139
152
|
is_workflow: is_workflow,
|
|
140
153
|
workflow_type: workflow_type,
|
|
141
154
|
workflow_children: workflow_children,
|
|
@@ -209,6 +222,32 @@ module RubyLLM
|
|
|
209
222
|
end
|
|
210
223
|
end
|
|
211
224
|
|
|
225
|
+
# Detects the agent type from class hierarchy
|
|
226
|
+
#
|
|
227
|
+
# @param agent_class [Class, nil] The agent class
|
|
228
|
+
# @return [String] "agent", "workflow", "embedder", "moderator", "speaker", "transcriber", or "image_generator"
|
|
229
|
+
def detect_agent_type(agent_class)
|
|
230
|
+
return "agent" unless agent_class
|
|
231
|
+
|
|
232
|
+
ancestors = agent_class.ancestors.map { |a| a.name.to_s }
|
|
233
|
+
|
|
234
|
+
if ancestors.include?("RubyLLM::Agents::Embedder")
|
|
235
|
+
"embedder"
|
|
236
|
+
elsif ancestors.include?("RubyLLM::Agents::Moderator")
|
|
237
|
+
"moderator"
|
|
238
|
+
elsif ancestors.include?("RubyLLM::Agents::Speaker")
|
|
239
|
+
"speaker"
|
|
240
|
+
elsif ancestors.include?("RubyLLM::Agents::Transcriber")
|
|
241
|
+
"transcriber"
|
|
242
|
+
elsif ancestors.include?("RubyLLM::Agents::ImageGenerator")
|
|
243
|
+
"image_generator"
|
|
244
|
+
elsif ancestors.include?("RubyLLM::Agents::Workflow")
|
|
245
|
+
"workflow"
|
|
246
|
+
else
|
|
247
|
+
"agent"
|
|
248
|
+
end
|
|
249
|
+
end
|
|
250
|
+
|
|
212
251
|
# Extracts child agents from workflow DSL configuration
|
|
213
252
|
#
|
|
214
253
|
# @param agent_class [Class, nil] The workflow class
|