ruby_llm-agents 0.3.6 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +46 -13
- data/app/controllers/ruby_llm/agents/api_configurations_controller.rb +214 -0
- 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/models/ruby_llm/agents/api_configuration.rb +386 -0
- data/app/models/ruby_llm/agents/tenant_budget.rb +62 -7
- data/app/views/layouts/ruby_llm/agents/application.html.erb +3 -1
- 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/executions/show.html.erb +82 -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/config/routes.rb +12 -1
- data/lib/generators/ruby_llm_agents/api_configuration_generator.rb +100 -0
- data/lib/generators/ruby_llm_agents/templates/create_api_configurations_migration.rb.tt +90 -0
- data/lib/ruby_llm/agents/base/dsl.rb +65 -13
- data/lib/ruby_llm/agents/base/execution.rb +113 -6
- data/lib/ruby_llm/agents/base/reliability_dsl.rb +82 -0
- data/lib/ruby_llm/agents/base.rb +28 -0
- data/lib/ruby_llm/agents/budget_tracker.rb +285 -23
- data/lib/ruby_llm/agents/configuration.rb +38 -1
- data/lib/ruby_llm/agents/deprecations.rb +77 -0
- data/lib/ruby_llm/agents/engine.rb +1 -0
- data/lib/ruby_llm/agents/instrumentation.rb +71 -3
- data/lib/ruby_llm/agents/reliability/breaker_manager.rb +80 -0
- data/lib/ruby_llm/agents/reliability/execution_constraints.rb +69 -0
- data/lib/ruby_llm/agents/reliability/executor.rb +124 -0
- data/lib/ruby_llm/agents/reliability/fallback_routing.rb +72 -0
- data/lib/ruby_llm/agents/reliability/retry_strategy.rb +76 -0
- data/lib/ruby_llm/agents/resolved_config.rb +348 -0
- data/lib/ruby_llm/agents/result.rb +72 -2
- data/lib/ruby_llm/agents/version.rb +1 -1
- data/lib/ruby_llm/agents.rb +6 -0
- metadata +26 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 4b0d9c86b501d77defab3fb33ae73b69548d332dad116dac76a56ac1dcaa3a60
|
|
4
|
+
data.tar.gz: 6ead43212abcb265fbb73b8daec26b49f8558c8962630d42279e3cf67328b263
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f5307aba39ba108d6104a39fcf842ab262d93237f07cc208291403f4fd1d23f604783b915c9236e85551caa0bf6e95bbe253159b4893cf174c3fb1dd12febe29
|
|
7
|
+
data.tar.gz: 0a72c3e6ae5912a9b694bd821ce514f6f4de8af7c99487d88bc9051ee80122f2f5e0e7bf97ec698d951a1e70012e676b15a21b9abbb22c777c9b677463d567e7
|
data/README.md
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# RubyLLM::Agents
|
|
2
2
|
|
|
3
|
+
> **AI Agents:** For comprehensive documentation optimized for AI consumption, see [LLMS.txt](LLMS.txt)
|
|
4
|
+
|
|
3
5
|
> **Production-ready Rails engine for building, managing, and monitoring LLM-powered AI agents**
|
|
4
6
|
|
|
5
7
|
[](https://rubygems.org/gems/ruby_llm-agents)
|
|
@@ -22,11 +24,12 @@ Build intelligent AI agents in Ruby with a clean DSL, automatic execution tracki
|
|
|
22
24
|
|
|
23
25
|
| Feature | Description | Docs |
|
|
24
26
|
|---------|-------------|------|
|
|
25
|
-
| **Agent DSL** | Declarative configuration with model, temperature, parameters | [Agent DSL](https://github.com/adham90/ruby_llm-agents/wiki/Agent-DSL) |
|
|
26
|
-
| **Execution Tracking** | Automatic logging with token usage
|
|
27
|
-
| **Cost Analytics** | Track spending by agent, model, and time period | [Analytics](https://github.com/adham90/ruby_llm-agents/wiki/Execution-Tracking) |
|
|
28
|
-
| **Reliability** | Automatic retries, model fallbacks, circuit breakers | [Reliability](https://github.com/adham90/ruby_llm-agents/wiki/Reliability) |
|
|
27
|
+
| **Agent DSL** | Declarative configuration with model, temperature, parameters, description | [Agent DSL](https://github.com/adham90/ruby_llm-agents/wiki/Agent-DSL) |
|
|
28
|
+
| **Execution Tracking** | Automatic logging with token usage, cost analytics, and fallback tracking | [Tracking](https://github.com/adham90/ruby_llm-agents/wiki/Execution-Tracking) |
|
|
29
|
+
| **Cost Analytics** | Track spending by agent, model, tenant, and time period | [Analytics](https://github.com/adham90/ruby_llm-agents/wiki/Execution-Tracking) |
|
|
30
|
+
| **Reliability** | Automatic retries, model fallbacks, circuit breakers with block DSL | [Reliability](https://github.com/adham90/ruby_llm-agents/wiki/Reliability) |
|
|
29
31
|
| **Budget Controls** | Daily/monthly limits with hard and soft enforcement | [Budgets](https://github.com/adham90/ruby_llm-agents/wiki/Budget-Controls) |
|
|
32
|
+
| **Multi-Tenancy** | Per-tenant budgets, circuit breakers, and execution isolation | [Multi-Tenancy](https://github.com/adham90/ruby_llm-agents/wiki/Multi-Tenancy) |
|
|
30
33
|
| **Workflows** | Pipelines, parallel execution, conditional routers | [Workflows](https://github.com/adham90/ruby_llm-agents/wiki/Workflows) |
|
|
31
34
|
| **Dashboard** | Real-time Turbo-powered monitoring UI | [Dashboard](https://github.com/adham90/ruby_llm-agents/wiki/Dashboard) |
|
|
32
35
|
| **Streaming** | Real-time response streaming with TTFT tracking | [Streaming](https://github.com/adham90/ruby_llm-agents/wiki/Streaming) |
|
|
@@ -116,13 +119,18 @@ See [Conversation History](https://github.com/adham90/ruby_llm-agents/wiki/Conve
|
|
|
116
119
|
|
|
117
120
|
## Documentation
|
|
118
121
|
|
|
122
|
+
> **Note:** Wiki content lives in the [`wiki/`](wiki/) folder. To sync changes to the [GitHub Wiki](https://github.com/adham90/ruby_llm-agents/wiki), run `./scripts/sync-wiki.sh`.
|
|
123
|
+
|
|
119
124
|
| Guide | Description |
|
|
120
125
|
|-------|-------------|
|
|
121
126
|
| [Getting Started](https://github.com/adham90/ruby_llm-agents/wiki/Getting-Started) | Installation, configuration, first agent |
|
|
122
|
-
| [Agent DSL](https://github.com/adham90/ruby_llm-agents/wiki/Agent-DSL) | All DSL options: model, temperature, params, caching |
|
|
123
|
-
| [Reliability](https://github.com/adham90/ruby_llm-agents/wiki/Reliability) | Retries, fallbacks, circuit breakers, timeouts |
|
|
127
|
+
| [Agent DSL](https://github.com/adham90/ruby_llm-agents/wiki/Agent-DSL) | All DSL options: model, temperature, params, caching, description |
|
|
128
|
+
| [Reliability](https://github.com/adham90/ruby_llm-agents/wiki/Reliability) | Retries, fallbacks, circuit breakers, timeouts, reliability block |
|
|
124
129
|
| [Workflows](https://github.com/adham90/ruby_llm-agents/wiki/Workflows) | Pipelines, parallel execution, routers |
|
|
125
130
|
| [Budget Controls](https://github.com/adham90/ruby_llm-agents/wiki/Budget-Controls) | Spending limits, alerts, enforcement |
|
|
131
|
+
| [Multi-Tenancy](https://github.com/adham90/ruby_llm-agents/wiki/Multi-Tenancy) | Per-tenant budgets, isolation, configuration |
|
|
132
|
+
| [Testing Agents](https://github.com/adham90/ruby_llm-agents/wiki/Testing-Agents) | RSpec patterns, mocking, dry_run mode |
|
|
133
|
+
| [Error Handling](https://github.com/adham90/ruby_llm-agents/wiki/Error-Handling) | Error types, recovery patterns |
|
|
126
134
|
| [Dashboard](https://github.com/adham90/ruby_llm-agents/wiki/Dashboard) | Setup, authentication, analytics |
|
|
127
135
|
| [Production](https://github.com/adham90/ruby_llm-agents/wiki/Production-Deployment) | Deployment best practices, background jobs |
|
|
128
136
|
| [API Reference](https://github.com/adham90/ruby_llm-agents/wiki/API-Reference) | Complete class documentation |
|
|
@@ -135,19 +143,22 @@ Build resilient agents with built-in fault tolerance:
|
|
|
135
143
|
```ruby
|
|
136
144
|
class ReliableAgent < ApplicationAgent
|
|
137
145
|
model "gpt-4o"
|
|
146
|
+
description "A resilient agent with automatic retries and fallbacks"
|
|
138
147
|
|
|
139
|
-
#
|
|
148
|
+
# Option 1: Individual DSL methods
|
|
140
149
|
retries max: 3, backoff: :exponential
|
|
141
|
-
|
|
142
|
-
# Fall back to alternative models
|
|
143
150
|
fallback_models "gpt-4o-mini", "claude-3-5-sonnet"
|
|
144
|
-
|
|
145
|
-
# Prevent cascading failures
|
|
146
151
|
circuit_breaker errors: 10, within: 60, cooldown: 300
|
|
147
|
-
|
|
148
|
-
# Maximum time for all attempts
|
|
149
152
|
total_timeout 30
|
|
150
153
|
|
|
154
|
+
# Option 2: Grouped reliability block (equivalent to above)
|
|
155
|
+
reliability do
|
|
156
|
+
retries max: 3, backoff: :exponential
|
|
157
|
+
fallback_models "gpt-4o-mini", "claude-3-5-sonnet"
|
|
158
|
+
circuit_breaker errors: 10, within: 60, cooldown: 300
|
|
159
|
+
total_timeout 30
|
|
160
|
+
end
|
|
161
|
+
|
|
151
162
|
param :query, required: true
|
|
152
163
|
|
|
153
164
|
def user_prompt
|
|
@@ -156,6 +167,28 @@ class ReliableAgent < ApplicationAgent
|
|
|
156
167
|
end
|
|
157
168
|
```
|
|
158
169
|
|
|
170
|
+
### Enhanced Result Object
|
|
171
|
+
|
|
172
|
+
The result object provides detailed execution metadata:
|
|
173
|
+
|
|
174
|
+
```ruby
|
|
175
|
+
result = ReliableAgent.call(query: "test")
|
|
176
|
+
|
|
177
|
+
# Basic response
|
|
178
|
+
result.content # => { ... }
|
|
179
|
+
result.success? # => true
|
|
180
|
+
|
|
181
|
+
# Reliability info
|
|
182
|
+
result.attempts_count # => 2 (if retry occurred)
|
|
183
|
+
result.used_fallback? # => true (if fallback model used)
|
|
184
|
+
result.chosen_model_id # => "gpt-4o-mini" (actual model used)
|
|
185
|
+
|
|
186
|
+
# Cost & timing
|
|
187
|
+
result.total_cost # => 0.00025
|
|
188
|
+
result.total_tokens # => 150
|
|
189
|
+
result.duration_ms # => 850
|
|
190
|
+
```
|
|
191
|
+
|
|
159
192
|
## Workflow Orchestration
|
|
160
193
|
|
|
161
194
|
Compose agents into complex workflows:
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
module Agents
|
|
5
|
+
# Controller for managing API configurations
|
|
6
|
+
#
|
|
7
|
+
# Provides CRUD operations for global and tenant-specific API
|
|
8
|
+
# configurations, including API keys and connection settings.
|
|
9
|
+
#
|
|
10
|
+
# @see ApiConfiguration
|
|
11
|
+
# @api private
|
|
12
|
+
class ApiConfigurationsController < ApplicationController
|
|
13
|
+
before_action :ensure_table_exists
|
|
14
|
+
before_action :set_global_config, only: [:show, :edit, :update]
|
|
15
|
+
before_action :set_tenant_config, only: [:tenant, :edit_tenant, :update_tenant]
|
|
16
|
+
|
|
17
|
+
# Displays the global API configuration
|
|
18
|
+
#
|
|
19
|
+
# @return [void]
|
|
20
|
+
def show
|
|
21
|
+
@resolved = ApiConfiguration.resolve
|
|
22
|
+
@provider_statuses = @resolved.provider_statuses_with_source
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Renders the edit form for global configuration
|
|
26
|
+
#
|
|
27
|
+
# @return [void]
|
|
28
|
+
def edit
|
|
29
|
+
@resolved = ApiConfiguration.resolve
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Updates the global API configuration
|
|
33
|
+
#
|
|
34
|
+
# @return [void]
|
|
35
|
+
def update
|
|
36
|
+
if @config.update(api_configuration_params)
|
|
37
|
+
log_configuration_change(@config, "global")
|
|
38
|
+
redirect_to edit_api_configuration_path, notice: "API configuration updated successfully"
|
|
39
|
+
else
|
|
40
|
+
render :edit, status: :unprocessable_entity
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Displays tenant-specific API configuration
|
|
45
|
+
#
|
|
46
|
+
# @return [void]
|
|
47
|
+
def tenant
|
|
48
|
+
@resolved = ApiConfiguration.resolve(tenant_id: params[:tenant_id])
|
|
49
|
+
@provider_statuses = @resolved.provider_statuses_with_source
|
|
50
|
+
@tenant_budget = TenantBudget.for_tenant(params[:tenant_id])
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Renders the edit form for tenant configuration
|
|
54
|
+
#
|
|
55
|
+
# @return [void]
|
|
56
|
+
def edit_tenant
|
|
57
|
+
@resolved = ApiConfiguration.resolve(tenant_id: @tenant_id)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Updates a tenant-specific API configuration
|
|
61
|
+
#
|
|
62
|
+
# @return [void]
|
|
63
|
+
def update_tenant
|
|
64
|
+
if @config.update(api_configuration_params)
|
|
65
|
+
log_configuration_change(@config, "tenant:#{params[:tenant_id]}")
|
|
66
|
+
redirect_to edit_tenant_api_configuration_path(params[:tenant_id]),
|
|
67
|
+
notice: "Tenant API configuration updated successfully"
|
|
68
|
+
else
|
|
69
|
+
render :edit_tenant, status: :unprocessable_entity
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Tests API key validity for a specific provider
|
|
74
|
+
# (Optional - can be used for AJAX validation)
|
|
75
|
+
#
|
|
76
|
+
# @return [void]
|
|
77
|
+
def test_connection
|
|
78
|
+
provider = params[:provider]
|
|
79
|
+
api_key = params[:api_key]
|
|
80
|
+
|
|
81
|
+
result = test_provider_connection(provider, api_key)
|
|
82
|
+
|
|
83
|
+
render json: {
|
|
84
|
+
success: result[:success],
|
|
85
|
+
message: result[:message],
|
|
86
|
+
models: result[:models]
|
|
87
|
+
}
|
|
88
|
+
rescue StandardError => e
|
|
89
|
+
render json: { success: false, message: e.message }
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
private
|
|
93
|
+
|
|
94
|
+
# Ensures the api_configurations table exists
|
|
95
|
+
def ensure_table_exists
|
|
96
|
+
return if ApiConfiguration.table_exists?
|
|
97
|
+
|
|
98
|
+
flash[:alert] = "API configurations table not found. Run the generator: rails generate ruby_llm_agents:api_configuration"
|
|
99
|
+
redirect_to root_path
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# Sets the global configuration (creates if not exists)
|
|
103
|
+
def set_global_config
|
|
104
|
+
@config = ApiConfiguration.global
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# Sets the tenant-specific configuration (creates if not exists)
|
|
108
|
+
def set_tenant_config
|
|
109
|
+
@tenant_id = params[:tenant_id]
|
|
110
|
+
raise ActionController::RoutingError, "Tenant ID required" if @tenant_id.blank?
|
|
111
|
+
|
|
112
|
+
@config = ApiConfiguration.for_tenant!(@tenant_id)
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# Strong parameters for API configuration
|
|
116
|
+
#
|
|
117
|
+
# @return [ActionController::Parameters]
|
|
118
|
+
def api_configuration_params
|
|
119
|
+
params.require(:api_configuration).permit(
|
|
120
|
+
# API Keys
|
|
121
|
+
:openai_api_key,
|
|
122
|
+
:anthropic_api_key,
|
|
123
|
+
:gemini_api_key,
|
|
124
|
+
:deepseek_api_key,
|
|
125
|
+
:mistral_api_key,
|
|
126
|
+
:perplexity_api_key,
|
|
127
|
+
:openrouter_api_key,
|
|
128
|
+
:gpustack_api_key,
|
|
129
|
+
:xai_api_key,
|
|
130
|
+
:ollama_api_key,
|
|
131
|
+
# AWS Bedrock
|
|
132
|
+
:bedrock_api_key,
|
|
133
|
+
:bedrock_secret_key,
|
|
134
|
+
:bedrock_session_token,
|
|
135
|
+
:bedrock_region,
|
|
136
|
+
# Vertex AI
|
|
137
|
+
:vertexai_credentials,
|
|
138
|
+
:vertexai_project_id,
|
|
139
|
+
:vertexai_location,
|
|
140
|
+
# Endpoints
|
|
141
|
+
:openai_api_base,
|
|
142
|
+
:gemini_api_base,
|
|
143
|
+
:ollama_api_base,
|
|
144
|
+
:gpustack_api_base,
|
|
145
|
+
:xai_api_base,
|
|
146
|
+
# OpenAI Options
|
|
147
|
+
:openai_organization_id,
|
|
148
|
+
:openai_project_id,
|
|
149
|
+
# Default Models
|
|
150
|
+
:default_model,
|
|
151
|
+
:default_embedding_model,
|
|
152
|
+
:default_image_model,
|
|
153
|
+
:default_moderation_model,
|
|
154
|
+
# Connection Settings
|
|
155
|
+
:request_timeout,
|
|
156
|
+
:max_retries,
|
|
157
|
+
:retry_interval,
|
|
158
|
+
:retry_backoff_factor,
|
|
159
|
+
:retry_interval_randomness,
|
|
160
|
+
:http_proxy,
|
|
161
|
+
# Inheritance
|
|
162
|
+
:inherit_global_defaults
|
|
163
|
+
).tap do |permitted|
|
|
164
|
+
# Remove blank API keys to prevent overwriting with empty values
|
|
165
|
+
# This allows users to submit forms without touching existing keys
|
|
166
|
+
ApiConfiguration::API_KEY_ATTRIBUTES.each do |key|
|
|
167
|
+
permitted.delete(key) if permitted[key].blank?
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
# Logs configuration changes for audit purposes
|
|
173
|
+
#
|
|
174
|
+
# @param config [ApiConfiguration] The configuration that changed
|
|
175
|
+
# @param scope [String] The scope identifier
|
|
176
|
+
def log_configuration_change(config, scope)
|
|
177
|
+
changed_fields = config.previous_changes.keys.reject { |k| k.end_with?("_at") }
|
|
178
|
+
return if changed_fields.empty?
|
|
179
|
+
|
|
180
|
+
# Mask sensitive fields in the log
|
|
181
|
+
masked_changes = changed_fields.map do |field|
|
|
182
|
+
if field.include?("api_key") || field.include?("secret") || field.include?("credentials")
|
|
183
|
+
"#{field}: [REDACTED]"
|
|
184
|
+
else
|
|
185
|
+
"#{field}: #{config.previous_changes[field].last}"
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
Rails.logger.info(
|
|
190
|
+
"[RubyLLM::Agents] API configuration updated for #{scope}: #{masked_changes.join(', ')}"
|
|
191
|
+
)
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
# Tests connection to a specific provider
|
|
195
|
+
#
|
|
196
|
+
# @param provider [String] Provider key
|
|
197
|
+
# @param api_key [String] API key to test
|
|
198
|
+
# @return [Hash] Test result with success, message, and models
|
|
199
|
+
def test_provider_connection(provider, api_key)
|
|
200
|
+
# This is a placeholder - actual implementation would depend on
|
|
201
|
+
# RubyLLM's ability to list models or make a test request
|
|
202
|
+
case provider
|
|
203
|
+
when "openai"
|
|
204
|
+
# Example: Try to list models
|
|
205
|
+
{ success: true, message: "Connection successful", models: [] }
|
|
206
|
+
when "anthropic"
|
|
207
|
+
{ success: true, message: "Connection successful", models: [] }
|
|
208
|
+
else
|
|
209
|
+
{ success: false, message: "Provider not supported for testing" }
|
|
210
|
+
end
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
end
|
|
@@ -2,14 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
module RubyLLM
|
|
4
4
|
module Agents
|
|
5
|
-
# Controller for displaying
|
|
5
|
+
# Controller for displaying system configuration
|
|
6
6
|
#
|
|
7
7
|
# Shows all configuration options from RubyLLM::Agents.configuration
|
|
8
8
|
# in a read-only dashboard view.
|
|
9
9
|
#
|
|
10
10
|
# @api private
|
|
11
|
-
class
|
|
12
|
-
# Displays the
|
|
11
|
+
class SystemConfigController < ApplicationController
|
|
12
|
+
# Displays the system configuration
|
|
13
13
|
#
|
|
14
14
|
# @return [void]
|
|
15
15
|
def show
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
module Agents
|
|
5
|
+
# Controller for managing tenant budgets
|
|
6
|
+
#
|
|
7
|
+
# Provides CRUD operations for viewing and editing tenant budget
|
|
8
|
+
# configurations, including cost limits and token limits.
|
|
9
|
+
#
|
|
10
|
+
# @see TenantBudget For budget configuration model
|
|
11
|
+
# @api private
|
|
12
|
+
class TenantsController < ApplicationController
|
|
13
|
+
# Lists all tenant budgets
|
|
14
|
+
#
|
|
15
|
+
# @return [void]
|
|
16
|
+
def index
|
|
17
|
+
@tenants = TenantBudget.order(:name, :tenant_id)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Shows a single tenant's budget details
|
|
21
|
+
#
|
|
22
|
+
# @return [void]
|
|
23
|
+
def show
|
|
24
|
+
@tenant = TenantBudget.find(params[:id])
|
|
25
|
+
@executions = tenant_executions(@tenant.tenant_id).recent.limit(10)
|
|
26
|
+
@usage_stats = calculate_usage_stats(@tenant)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Renders the edit form for a tenant budget
|
|
30
|
+
#
|
|
31
|
+
# @return [void]
|
|
32
|
+
def edit
|
|
33
|
+
@tenant = TenantBudget.find(params[:id])
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Updates a tenant budget
|
|
37
|
+
#
|
|
38
|
+
# @return [void]
|
|
39
|
+
def update
|
|
40
|
+
@tenant = TenantBudget.find(params[:id])
|
|
41
|
+
if @tenant.update(tenant_params)
|
|
42
|
+
redirect_to tenant_path(@tenant), notice: "Tenant updated successfully"
|
|
43
|
+
else
|
|
44
|
+
render :edit, status: :unprocessable_entity
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
private
|
|
49
|
+
|
|
50
|
+
# Strong parameters for tenant budget
|
|
51
|
+
#
|
|
52
|
+
# @return [ActionController::Parameters] Permitted parameters
|
|
53
|
+
def tenant_params
|
|
54
|
+
params.require(:tenant_budget).permit(
|
|
55
|
+
:name, :daily_limit, :monthly_limit,
|
|
56
|
+
:daily_token_limit, :monthly_token_limit,
|
|
57
|
+
:enforcement
|
|
58
|
+
)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Returns executions scoped to a specific tenant
|
|
62
|
+
#
|
|
63
|
+
# @param tenant_id [String] The tenant identifier
|
|
64
|
+
# @return [ActiveRecord::Relation] Executions for the tenant
|
|
65
|
+
def tenant_executions(tenant_id)
|
|
66
|
+
Execution.by_tenant(tenant_id)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Calculates usage statistics for a tenant
|
|
70
|
+
#
|
|
71
|
+
# @param tenant [TenantBudget] The tenant budget record
|
|
72
|
+
# @return [Hash] Usage statistics
|
|
73
|
+
def calculate_usage_stats(tenant)
|
|
74
|
+
scope = tenant_executions(tenant.tenant_id)
|
|
75
|
+
today_scope = scope.where("created_at >= ?", Time.current.beginning_of_day)
|
|
76
|
+
month_scope = scope.where("created_at >= ?", Time.current.beginning_of_month)
|
|
77
|
+
|
|
78
|
+
daily_spend = today_scope.sum(:total_cost) || 0
|
|
79
|
+
monthly_spend = month_scope.sum(:total_cost) || 0
|
|
80
|
+
daily_tokens = today_scope.sum(:total_tokens) || 0
|
|
81
|
+
monthly_tokens = month_scope.sum(:total_tokens) || 0
|
|
82
|
+
|
|
83
|
+
{
|
|
84
|
+
daily_spend: daily_spend,
|
|
85
|
+
monthly_spend: monthly_spend,
|
|
86
|
+
daily_tokens: daily_tokens,
|
|
87
|
+
monthly_tokens: monthly_tokens,
|
|
88
|
+
daily_spend_percentage: percentage_used(daily_spend, tenant.effective_daily_limit),
|
|
89
|
+
monthly_spend_percentage: percentage_used(monthly_spend, tenant.effective_monthly_limit),
|
|
90
|
+
daily_token_percentage: percentage_used(daily_tokens, tenant.effective_daily_token_limit),
|
|
91
|
+
monthly_token_percentage: percentage_used(monthly_tokens, tenant.effective_monthly_token_limit),
|
|
92
|
+
total_executions: scope.count,
|
|
93
|
+
total_cost: scope.sum(:total_cost) || 0,
|
|
94
|
+
total_tokens: scope.sum(:total_tokens) || 0
|
|
95
|
+
}
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Calculates percentage used
|
|
99
|
+
#
|
|
100
|
+
# @param current [Numeric] Current usage
|
|
101
|
+
# @param limit [Numeric, nil] The limit
|
|
102
|
+
# @return [Float] Percentage used (0-100+)
|
|
103
|
+
def percentage_used(current, limit)
|
|
104
|
+
return 0 if limit.nil? || limit.to_f <= 0
|
|
105
|
+
(current.to_f / limit.to_f * 100).round(1)
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|