ruby_llm-agents 0.3.3 → 0.3.5
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 +132 -1263
- data/app/controllers/concerns/ruby_llm/agents/filterable.rb +5 -1
- data/app/controllers/concerns/ruby_llm/agents/paginatable.rb +2 -1
- data/app/controllers/ruby_llm/agents/agents_controller.rb +21 -2
- data/app/controllers/ruby_llm/agents/dashboard_controller.rb +137 -9
- data/app/controllers/ruby_llm/agents/executions_controller.rb +83 -5
- data/app/models/ruby_llm/agents/execution/analytics.rb +103 -12
- data/app/models/ruby_llm/agents/execution/scopes.rb +25 -0
- data/app/models/ruby_llm/agents/execution/workflow.rb +299 -0
- data/app/models/ruby_llm/agents/execution.rb +28 -59
- data/app/models/ruby_llm/agents/tenant_budget.rb +165 -0
- data/app/services/ruby_llm/agents/agent_registry.rb +118 -7
- data/app/views/layouts/ruby_llm/agents/application.html.erb +430 -0
- data/app/views/ruby_llm/agents/agents/_empty_state.html.erb +23 -0
- data/app/views/ruby_llm/agents/agents/_workflow.html.erb +125 -0
- data/app/views/ruby_llm/agents/agents/index.html.erb +93 -0
- data/app/views/ruby_llm/agents/agents/show.html.erb +775 -0
- data/app/views/ruby_llm/agents/dashboard/_agent_comparison.html.erb +112 -0
- data/app/views/ruby_llm/agents/dashboard/_budgets_bar.html.erb +75 -0
- data/app/views/{rubyllm → ruby_llm}/agents/dashboard/_execution_item.html.erb +7 -4
- data/app/views/ruby_llm/agents/dashboard/_now_strip.html.erb +84 -0
- data/app/views/ruby_llm/agents/dashboard/_tenant_budget.html.erb +115 -0
- data/app/views/ruby_llm/agents/dashboard/_top_errors.html.erb +49 -0
- data/app/views/ruby_llm/agents/dashboard/index.html.erb +155 -0
- data/app/views/{rubyllm → ruby_llm}/agents/executions/_execution.html.erb +1 -1
- data/app/views/{rubyllm → ruby_llm}/agents/executions/_filters.html.erb +39 -11
- data/app/views/{rubyllm → ruby_llm}/agents/executions/_list.html.erb +19 -9
- data/app/views/ruby_llm/agents/executions/_workflow_summary.html.erb +101 -0
- data/app/views/ruby_llm/agents/executions/index.html.erb +88 -0
- data/app/views/{rubyllm → ruby_llm}/agents/executions/show.html.erb +260 -200
- data/app/views/{rubyllm → ruby_llm}/agents/settings/show.html.erb +1 -1
- data/app/views/ruby_llm/agents/shared/_breadcrumbs.html.erb +48 -0
- data/app/views/ruby_llm/agents/shared/_executions_table.html.erb +251 -0
- data/app/views/{rubyllm → ruby_llm}/agents/shared/_filter_dropdown.html.erb +1 -1
- data/app/views/ruby_llm/agents/shared/_nav_link.html.erb +27 -0
- data/app/views/{rubyllm → ruby_llm}/agents/shared/_select_dropdown.html.erb +1 -1
- data/app/views/{rubyllm → ruby_llm}/agents/shared/_status_badge.html.erb +1 -1
- data/app/views/{rubyllm → ruby_llm}/agents/shared/_status_dot.html.erb +1 -1
- data/app/views/ruby_llm/agents/shared/_tenant_filter.html.erb +26 -0
- data/app/views/ruby_llm/agents/shared/_workflow_type_badge.html.erb +61 -0
- data/config/routes.rb +2 -0
- data/lib/generators/ruby_llm_agents/multi_tenancy_generator.rb +97 -0
- data/lib/generators/ruby_llm_agents/templates/add_attempts_migration.rb.tt +3 -3
- data/lib/generators/ruby_llm_agents/templates/add_tenant_to_executions_migration.rb.tt +23 -0
- data/lib/generators/ruby_llm_agents/templates/add_tool_calls_migration.rb.tt +2 -2
- data/lib/generators/ruby_llm_agents/templates/add_workflow_migration.rb.tt +38 -0
- data/lib/generators/ruby_llm_agents/templates/create_tenant_budgets_migration.rb.tt +45 -0
- data/lib/generators/ruby_llm_agents/templates/migration.rb.tt +17 -5
- data/lib/generators/ruby_llm_agents/upgrade_generator.rb +13 -0
- data/lib/ruby_llm/agents/alert_manager.rb +20 -16
- data/lib/ruby_llm/agents/base/caching.rb +40 -0
- data/lib/ruby_llm/agents/base/cost_calculation.rb +105 -0
- data/lib/ruby_llm/agents/base/dsl.rb +261 -0
- data/lib/ruby_llm/agents/base/execution.rb +258 -0
- data/lib/ruby_llm/agents/base/reliability_execution.rb +136 -0
- data/lib/ruby_llm/agents/base/response_building.rb +86 -0
- data/lib/ruby_llm/agents/base/tool_tracking.rb +57 -0
- data/lib/ruby_llm/agents/base.rb +37 -801
- data/lib/ruby_llm/agents/budget_tracker.rb +250 -139
- data/lib/ruby_llm/agents/cache_helper.rb +98 -0
- data/lib/ruby_llm/agents/circuit_breaker.rb +48 -30
- data/lib/ruby_llm/agents/configuration.rb +40 -1
- data/lib/ruby_llm/agents/engine.rb +65 -1
- data/lib/ruby_llm/agents/inflections.rb +14 -0
- data/lib/ruby_llm/agents/instrumentation.rb +66 -0
- data/lib/ruby_llm/agents/reliability.rb +8 -2
- data/lib/ruby_llm/agents/version.rb +1 -1
- data/lib/ruby_llm/agents/workflow/instrumentation.rb +254 -0
- data/lib/ruby_llm/agents/workflow/parallel.rb +282 -0
- data/lib/ruby_llm/agents/workflow/pipeline.rb +306 -0
- data/lib/ruby_llm/agents/workflow/result.rb +390 -0
- data/lib/ruby_llm/agents/workflow/router.rb +429 -0
- data/lib/ruby_llm/agents/workflow.rb +232 -0
- data/lib/ruby_llm/agents.rb +1 -0
- metadata +57 -75
- data/app/channels/ruby_llm/agents/executions_channel.rb +0 -46
- data/app/javascript/ruby_llm/agents/controllers/filter_controller.js +0 -56
- data/app/javascript/ruby_llm/agents/controllers/index.js +0 -12
- data/app/javascript/ruby_llm/agents/controllers/refresh_controller.js +0 -83
- data/app/views/layouts/rubyllm/agents/application.html.erb +0 -626
- data/app/views/rubyllm/agents/agents/index.html.erb +0 -20
- data/app/views/rubyllm/agents/agents/show.html.erb +0 -772
- data/app/views/rubyllm/agents/dashboard/_budgets_bar.html.erb +0 -165
- data/app/views/rubyllm/agents/dashboard/_now_strip.html.erb +0 -10
- data/app/views/rubyllm/agents/dashboard/_now_strip_values.html.erb +0 -71
- data/app/views/rubyllm/agents/dashboard/index.html.erb +0 -197
- data/app/views/rubyllm/agents/executions/index.html.erb +0 -28
- data/app/views/rubyllm/agents/executions/index.turbo_stream.erb +0 -18
- data/app/views/rubyllm/agents/shared/_executions_table.html.erb +0 -193
- /data/app/views/{rubyllm → ruby_llm}/agents/agents/_agent.html.erb +0 -0
- /data/app/views/{rubyllm → ruby_llm}/agents/agents/_version_comparison.html.erb +0 -0
- /data/app/views/{rubyllm → ruby_llm}/agents/dashboard/_action_center.html.erb +0 -0
- /data/app/views/{rubyllm → ruby_llm}/agents/dashboard/_alerts_feed.html.erb +0 -0
- /data/app/views/{rubyllm → ruby_llm}/agents/dashboard/_breaker_strip.html.erb +0 -0
- /data/app/views/{rubyllm → ruby_llm}/agents/executions/dry_run.html.erb +0 -0
- /data/app/views/{rubyllm → ruby_llm}/agents/shared/_stat_card.html.erb +0 -0
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Migration to create the tenant_budgets table for multi-tenancy support
|
|
4
|
+
#
|
|
5
|
+
# This table stores per-tenant budget configuration, allowing different
|
|
6
|
+
# tenants to have their own budget limits and enforcement modes.
|
|
7
|
+
#
|
|
8
|
+
# Features:
|
|
9
|
+
# - Per-tenant daily and monthly budget limits
|
|
10
|
+
# - Per-agent budget limits within a tenant
|
|
11
|
+
# - Configurable enforcement mode (none, soft, hard)
|
|
12
|
+
# - Option to inherit global defaults for unset limits
|
|
13
|
+
#
|
|
14
|
+
# Run with: rails db:migrate
|
|
15
|
+
class CreateRubyLLMAgentsTenantBudgets < ActiveRecord::Migration<%= migration_version %>
|
|
16
|
+
def change
|
|
17
|
+
create_table :ruby_llm_agents_tenant_budgets do |t|
|
|
18
|
+
# Unique identifier for the tenant (e.g., organization ID, workspace ID)
|
|
19
|
+
t.string :tenant_id, null: false
|
|
20
|
+
|
|
21
|
+
# Global budget limits for this tenant
|
|
22
|
+
t.decimal :daily_limit, precision: 12, scale: 6
|
|
23
|
+
t.decimal :monthly_limit, precision: 12, scale: 6
|
|
24
|
+
|
|
25
|
+
# Per-agent budget limits (JSON hash)
|
|
26
|
+
# Format: { "AgentName" => limit_value }
|
|
27
|
+
t.json :per_agent_daily, null: false, default: {}
|
|
28
|
+
t.json :per_agent_monthly, null: false, default: {}
|
|
29
|
+
|
|
30
|
+
# Enforcement mode for this tenant: "none", "soft", or "hard"
|
|
31
|
+
# - none: no enforcement, only tracking
|
|
32
|
+
# - soft: log warnings when limits exceeded
|
|
33
|
+
# - hard: block execution when limits exceeded
|
|
34
|
+
t.string :enforcement, default: "soft"
|
|
35
|
+
|
|
36
|
+
# Whether to inherit from global config for unset limits
|
|
37
|
+
t.boolean :inherit_global_defaults, default: true
|
|
38
|
+
|
|
39
|
+
t.timestamps
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Ensure unique tenant IDs
|
|
43
|
+
add_index :ruby_llm_agents_tenant_budgets, :tenant_id, unique: true
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -54,10 +54,10 @@ class CreateRubyLLMAgentsExecutions < ActiveRecord::Migration<%= migration_versi
|
|
|
54
54
|
t.decimal :output_cost, precision: 12, scale: 6
|
|
55
55
|
t.decimal :total_cost, precision: 12, scale: 6
|
|
56
56
|
|
|
57
|
-
# Data (
|
|
58
|
-
t.
|
|
59
|
-
t.
|
|
60
|
-
t.
|
|
57
|
+
# Data (JSON - works with PostgreSQL, MySQL, SQLite3)
|
|
58
|
+
t.json :parameters, null: false, default: {}
|
|
59
|
+
t.json :response, default: {}
|
|
60
|
+
t.json :metadata, null: false, default: {}
|
|
61
61
|
|
|
62
62
|
# Error tracking
|
|
63
63
|
t.string :error_class
|
|
@@ -68,9 +68,16 @@ class CreateRubyLLMAgentsExecutions < ActiveRecord::Migration<%= migration_versi
|
|
|
68
68
|
t.text :user_prompt
|
|
69
69
|
|
|
70
70
|
# Tool calls tracking
|
|
71
|
-
t.
|
|
71
|
+
t.json :tool_calls, null: false, default: []
|
|
72
72
|
t.integer :tool_calls_count, null: false, default: 0
|
|
73
73
|
|
|
74
|
+
# Workflow orchestration
|
|
75
|
+
t.string :workflow_id
|
|
76
|
+
t.string :workflow_type
|
|
77
|
+
t.string :workflow_step
|
|
78
|
+
t.string :routed_to
|
|
79
|
+
t.json :classification_result
|
|
80
|
+
|
|
74
81
|
t.timestamps
|
|
75
82
|
end
|
|
76
83
|
|
|
@@ -96,6 +103,11 @@ class CreateRubyLLMAgentsExecutions < ActiveRecord::Migration<%= migration_versi
|
|
|
96
103
|
# Tool calls index
|
|
97
104
|
add_index :ruby_llm_agents_executions, :tool_calls_count
|
|
98
105
|
|
|
106
|
+
# Workflow indexes
|
|
107
|
+
add_index :ruby_llm_agents_executions, :workflow_id
|
|
108
|
+
add_index :ruby_llm_agents_executions, :workflow_type
|
|
109
|
+
add_index :ruby_llm_agents_executions, [:workflow_id, :workflow_step]
|
|
110
|
+
|
|
99
111
|
# Foreign keys for execution hierarchy
|
|
100
112
|
add_foreign_key :ruby_llm_agents_executions, :ruby_llm_agents_executions,
|
|
101
113
|
column: :parent_execution_id, on_delete: :nullify
|
|
@@ -120,6 +120,19 @@ module RubyLlmAgents
|
|
|
120
120
|
)
|
|
121
121
|
end
|
|
122
122
|
|
|
123
|
+
def create_add_workflow_migration
|
|
124
|
+
# Check if columns already exist
|
|
125
|
+
if column_exists?(:ruby_llm_agents_executions, :workflow_id)
|
|
126
|
+
say_status :skip, "workflow_id column already exists", :yellow
|
|
127
|
+
return
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
migration_template(
|
|
131
|
+
"add_workflow_migration.rb.tt",
|
|
132
|
+
File.join(db_migrate_path, "add_workflow_to_ruby_llm_agents_executions.rb")
|
|
133
|
+
)
|
|
134
|
+
end
|
|
135
|
+
|
|
123
136
|
def show_post_upgrade_message
|
|
124
137
|
say ""
|
|
125
138
|
say "RubyLLM::Agents upgrade migration created!", :green
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "
|
|
3
|
+
require "faraday"
|
|
4
4
|
require "json"
|
|
5
5
|
|
|
6
6
|
module RubyLLM
|
|
@@ -177,30 +177,34 @@ module RubyLLM
|
|
|
177
177
|
end
|
|
178
178
|
end
|
|
179
179
|
|
|
180
|
-
# Posts JSON to a URL
|
|
180
|
+
# Posts JSON to a URL using Faraday
|
|
181
181
|
#
|
|
182
182
|
# @param url [String] The URL
|
|
183
183
|
# @param payload [Hash] The payload
|
|
184
|
-
# @return [
|
|
184
|
+
# @return [Faraday::Response]
|
|
185
185
|
def post_json(url, payload)
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
http.read_timeout = 10
|
|
191
|
-
|
|
192
|
-
request = Net::HTTP::Post.new(uri.request_uri)
|
|
193
|
-
request["Content-Type"] = "application/json"
|
|
194
|
-
request.body = payload.to_json
|
|
195
|
-
|
|
196
|
-
response = http.request(request)
|
|
186
|
+
response = http_client.post(url) do |req|
|
|
187
|
+
req.headers["Content-Type"] = "application/json"
|
|
188
|
+
req.body = payload.to_json
|
|
189
|
+
end
|
|
197
190
|
|
|
198
|
-
unless response.
|
|
199
|
-
Rails.logger.warn("[RubyLLM::Agents::AlertManager] Webhook returned #{response.
|
|
191
|
+
unless response.success?
|
|
192
|
+
Rails.logger.warn("[RubyLLM::Agents::AlertManager] Webhook returned #{response.status}: #{response.body}")
|
|
200
193
|
end
|
|
201
194
|
|
|
202
195
|
response
|
|
203
196
|
end
|
|
197
|
+
|
|
198
|
+
# Returns a configured Faraday HTTP client
|
|
199
|
+
#
|
|
200
|
+
# @return [Faraday::Connection]
|
|
201
|
+
def http_client
|
|
202
|
+
@http_client ||= Faraday.new do |conn|
|
|
203
|
+
conn.options.open_timeout = 5
|
|
204
|
+
conn.options.timeout = 10
|
|
205
|
+
conn.adapter Faraday.default_adapter
|
|
206
|
+
end
|
|
207
|
+
end
|
|
204
208
|
end
|
|
205
209
|
end
|
|
206
210
|
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../cache_helper"
|
|
4
|
+
|
|
5
|
+
module RubyLLM
|
|
6
|
+
module Agents
|
|
7
|
+
class Base
|
|
8
|
+
# Cache management for agent responses
|
|
9
|
+
#
|
|
10
|
+
# Handles cache key generation and store access for
|
|
11
|
+
# caching agent execution results.
|
|
12
|
+
module Caching
|
|
13
|
+
include CacheHelper
|
|
14
|
+
|
|
15
|
+
# Generates the full cache key for this agent invocation
|
|
16
|
+
#
|
|
17
|
+
# @return [String] Cache key in format "ruby_llm_agent/ClassName/version/hash"
|
|
18
|
+
def agent_cache_key
|
|
19
|
+
["ruby_llm_agent", self.class.name, self.class.version, cache_key_hash].join("/")
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Generates a hash of the cache key data
|
|
23
|
+
#
|
|
24
|
+
# @return [String] SHA256 hex digest of the cache key data
|
|
25
|
+
def cache_key_hash
|
|
26
|
+
Digest::SHA256.hexdigest(cache_key_data.to_json)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Returns data to include in cache key generation
|
|
30
|
+
#
|
|
31
|
+
# Override to customize what parameters affect cache invalidation.
|
|
32
|
+
#
|
|
33
|
+
# @return [Hash] Data to hash for cache key
|
|
34
|
+
def cache_key_data
|
|
35
|
+
@options.except(:skip_cache, :dry_run, :with)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
module Agents
|
|
5
|
+
class Base
|
|
6
|
+
# Cost calculation methods for token and pricing calculations
|
|
7
|
+
#
|
|
8
|
+
# Handles input/output cost calculations, model info resolution,
|
|
9
|
+
# and budget tracking for agent executions.
|
|
10
|
+
module CostCalculation
|
|
11
|
+
# Calculates input cost from tokens
|
|
12
|
+
#
|
|
13
|
+
# @param input_tokens [Integer, nil] Number of input tokens
|
|
14
|
+
# @param response_model_id [String, nil] Model that responded
|
|
15
|
+
# @return [Float, nil] Input cost in USD
|
|
16
|
+
def result_input_cost(input_tokens, response_model_id)
|
|
17
|
+
return nil unless input_tokens
|
|
18
|
+
model_info = result_model_info(response_model_id)
|
|
19
|
+
return nil unless model_info&.pricing
|
|
20
|
+
price = model_info.pricing.text_tokens&.input || 0
|
|
21
|
+
(input_tokens / 1_000_000.0 * price).round(6)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Calculates output cost from tokens
|
|
25
|
+
#
|
|
26
|
+
# @param output_tokens [Integer, nil] Number of output tokens
|
|
27
|
+
# @param response_model_id [String, nil] Model that responded
|
|
28
|
+
# @return [Float, nil] Output cost in USD
|
|
29
|
+
def result_output_cost(output_tokens, response_model_id)
|
|
30
|
+
return nil unless output_tokens
|
|
31
|
+
model_info = result_model_info(response_model_id)
|
|
32
|
+
return nil unless model_info&.pricing
|
|
33
|
+
price = model_info.pricing.text_tokens&.output || 0
|
|
34
|
+
(output_tokens / 1_000_000.0 * price).round(6)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Calculates total cost from tokens
|
|
38
|
+
#
|
|
39
|
+
# @param input_tokens [Integer, nil] Number of input tokens
|
|
40
|
+
# @param output_tokens [Integer, nil] Number of output tokens
|
|
41
|
+
# @param response_model_id [String, nil] Model that responded
|
|
42
|
+
# @return [Float, nil] Total cost in USD
|
|
43
|
+
def result_total_cost(input_tokens, output_tokens, response_model_id)
|
|
44
|
+
input_cost = result_input_cost(input_tokens, response_model_id)
|
|
45
|
+
output_cost = result_output_cost(output_tokens, response_model_id)
|
|
46
|
+
return nil unless input_cost || output_cost
|
|
47
|
+
((input_cost || 0) + (output_cost || 0)).round(6)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Resolves model info for cost calculation
|
|
51
|
+
#
|
|
52
|
+
# @param response_model_id [String, nil] Model ID from response
|
|
53
|
+
# @return [Object, nil] Model info or nil
|
|
54
|
+
def result_model_info(response_model_id)
|
|
55
|
+
lookup_id = response_model_id || model
|
|
56
|
+
return nil unless lookup_id
|
|
57
|
+
model_obj, _provider = RubyLLM::Models.resolve(lookup_id)
|
|
58
|
+
model_obj
|
|
59
|
+
rescue StandardError
|
|
60
|
+
nil
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Resolves model info for cost calculation (alternate method)
|
|
64
|
+
#
|
|
65
|
+
# @param model_id [String] The model identifier
|
|
66
|
+
# @return [Object, nil] Model info or nil
|
|
67
|
+
def resolve_model_info(model_id)
|
|
68
|
+
model_obj, _provider = RubyLLM::Models.resolve(model_id)
|
|
69
|
+
model_obj
|
|
70
|
+
rescue StandardError
|
|
71
|
+
nil
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Records cost from an attempt to the budget tracker
|
|
75
|
+
#
|
|
76
|
+
# @param attempt_tracker [AttemptTracker] The attempt tracker
|
|
77
|
+
# @param tenant_id [String, nil] Optional tenant identifier for multi-tenant tracking
|
|
78
|
+
# @return [void]
|
|
79
|
+
def record_attempt_cost(attempt_tracker, tenant_id: nil)
|
|
80
|
+
successful = attempt_tracker.successful_attempt
|
|
81
|
+
return unless successful
|
|
82
|
+
|
|
83
|
+
# Calculate cost for this execution
|
|
84
|
+
# Note: Full cost calculation happens in instrumentation, but we
|
|
85
|
+
# record the spend here for budget tracking
|
|
86
|
+
model_info = resolve_model_info(successful[:model_id])
|
|
87
|
+
return unless model_info&.pricing
|
|
88
|
+
|
|
89
|
+
input_tokens = successful[:input_tokens] || 0
|
|
90
|
+
output_tokens = successful[:output_tokens] || 0
|
|
91
|
+
|
|
92
|
+
input_price = model_info.pricing.text_tokens&.input || 0
|
|
93
|
+
output_price = model_info.pricing.text_tokens&.output || 0
|
|
94
|
+
|
|
95
|
+
total_cost = (input_tokens / 1_000_000.0 * input_price) +
|
|
96
|
+
(output_tokens / 1_000_000.0 * output_price)
|
|
97
|
+
|
|
98
|
+
BudgetTracker.record_spend!(self.class.name, total_cost, tenant_id: tenant_id)
|
|
99
|
+
rescue StandardError => e
|
|
100
|
+
Rails.logger.warn("[RubyLLM::Agents] Failed to record budget spend: #{e.message}")
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
module Agents
|
|
5
|
+
class Base
|
|
6
|
+
# Class-level DSL for configuring agents
|
|
7
|
+
#
|
|
8
|
+
# Provides methods for setting model, temperature, timeout, caching,
|
|
9
|
+
# reliability, streaming, tools, and parameters.
|
|
10
|
+
module DSL
|
|
11
|
+
# @!visibility private
|
|
12
|
+
VERSION = "1.0"
|
|
13
|
+
# @!visibility private
|
|
14
|
+
CACHE_TTL = 1.hour
|
|
15
|
+
|
|
16
|
+
# @!group Configuration DSL
|
|
17
|
+
|
|
18
|
+
# Sets or returns the LLM model for this agent class
|
|
19
|
+
#
|
|
20
|
+
# @param value [String, nil] The model identifier to set
|
|
21
|
+
# @return [String] The current model setting
|
|
22
|
+
# @example
|
|
23
|
+
# model "gpt-4o"
|
|
24
|
+
def model(value = nil)
|
|
25
|
+
@model = value if value
|
|
26
|
+
@model || inherited_or_default(:model, RubyLLM::Agents.configuration.default_model)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Sets or returns the temperature for LLM responses
|
|
30
|
+
#
|
|
31
|
+
# @param value [Float, nil] Temperature value (0.0-2.0)
|
|
32
|
+
# @return [Float] The current temperature setting
|
|
33
|
+
# @example
|
|
34
|
+
# temperature 0.7
|
|
35
|
+
def temperature(value = nil)
|
|
36
|
+
@temperature = value if value
|
|
37
|
+
@temperature || inherited_or_default(:temperature, RubyLLM::Agents.configuration.default_temperature)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Sets or returns the version string for cache invalidation
|
|
41
|
+
#
|
|
42
|
+
# @param value [String, nil] Version string
|
|
43
|
+
# @return [String] The current version
|
|
44
|
+
# @example
|
|
45
|
+
# version "2.0"
|
|
46
|
+
def version(value = nil)
|
|
47
|
+
@version = value if value
|
|
48
|
+
@version || inherited_or_default(:version, VERSION)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Sets or returns the timeout in seconds for LLM requests
|
|
52
|
+
#
|
|
53
|
+
# @param value [Integer, nil] Timeout in seconds
|
|
54
|
+
# @return [Integer] The current timeout setting
|
|
55
|
+
# @example
|
|
56
|
+
# timeout 30
|
|
57
|
+
def timeout(value = nil)
|
|
58
|
+
@timeout = value if value
|
|
59
|
+
@timeout || inherited_or_default(:timeout, RubyLLM::Agents.configuration.default_timeout)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# @!endgroup
|
|
63
|
+
|
|
64
|
+
# @!group Reliability DSL
|
|
65
|
+
|
|
66
|
+
# Configures retry behavior for this agent
|
|
67
|
+
#
|
|
68
|
+
# @param max [Integer] Maximum number of retry attempts (default: 0)
|
|
69
|
+
# @param backoff [Symbol] Backoff strategy (:constant or :exponential)
|
|
70
|
+
# @param base [Float] Base delay in seconds
|
|
71
|
+
# @param max_delay [Float] Maximum delay between retries
|
|
72
|
+
# @param on [Array<Class>] Error classes to retry on (extends defaults)
|
|
73
|
+
# @return [Hash] The current retry configuration
|
|
74
|
+
# @example
|
|
75
|
+
# retries max: 2, backoff: :exponential, base: 0.4, max_delay: 3.0, on: [Timeout::Error]
|
|
76
|
+
def retries(max: nil, backoff: nil, base: nil, max_delay: nil, on: nil)
|
|
77
|
+
if max || backoff || base || max_delay || on
|
|
78
|
+
@retries_config ||= RubyLLM::Agents.configuration.default_retries.dup
|
|
79
|
+
@retries_config[:max] = max if max
|
|
80
|
+
@retries_config[:backoff] = backoff if backoff
|
|
81
|
+
@retries_config[:base] = base if base
|
|
82
|
+
@retries_config[:max_delay] = max_delay if max_delay
|
|
83
|
+
@retries_config[:on] = on if on
|
|
84
|
+
end
|
|
85
|
+
@retries_config || inherited_or_default(:retries_config, RubyLLM::Agents.configuration.default_retries)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Returns the retry configuration for this agent
|
|
89
|
+
#
|
|
90
|
+
# @return [Hash, nil] The retry configuration
|
|
91
|
+
def retries_config
|
|
92
|
+
@retries_config || (superclass.respond_to?(:retries_config) ? superclass.retries_config : nil)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Sets or returns fallback models to try when primary model fails
|
|
96
|
+
#
|
|
97
|
+
# @param models [Array<String>, nil] Model identifiers to use as fallbacks
|
|
98
|
+
# @return [Array<String>] The current fallback models
|
|
99
|
+
# @example
|
|
100
|
+
# fallback_models ["gpt-4o-mini", "gpt-4o"]
|
|
101
|
+
def fallback_models(models = nil)
|
|
102
|
+
@fallback_models = models if models
|
|
103
|
+
@fallback_models || inherited_or_default(:fallback_models, RubyLLM::Agents.configuration.default_fallback_models)
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Sets or returns the total timeout for all retry/fallback attempts
|
|
107
|
+
#
|
|
108
|
+
# @param seconds [Integer, nil] Total timeout in seconds
|
|
109
|
+
# @return [Integer, nil] The current total timeout
|
|
110
|
+
# @example
|
|
111
|
+
# total_timeout 20
|
|
112
|
+
def total_timeout(seconds = nil)
|
|
113
|
+
@total_timeout = seconds if seconds
|
|
114
|
+
@total_timeout || inherited_or_default(:total_timeout, RubyLLM::Agents.configuration.default_total_timeout)
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# Configures circuit breaker for this agent
|
|
118
|
+
#
|
|
119
|
+
# @param errors [Integer] Number of errors to trigger open state
|
|
120
|
+
# @param within [Integer] Rolling window in seconds
|
|
121
|
+
# @param cooldown [Integer] Cooldown period in seconds when open
|
|
122
|
+
# @return [Hash, nil] The current circuit breaker configuration
|
|
123
|
+
# @example
|
|
124
|
+
# circuit_breaker errors: 10, within: 60, cooldown: 300
|
|
125
|
+
def circuit_breaker(errors: nil, within: nil, cooldown: nil)
|
|
126
|
+
if errors || within || cooldown
|
|
127
|
+
@circuit_breaker_config ||= { errors: 10, within: 60, cooldown: 300 }
|
|
128
|
+
@circuit_breaker_config[:errors] = errors if errors
|
|
129
|
+
@circuit_breaker_config[:within] = within if within
|
|
130
|
+
@circuit_breaker_config[:cooldown] = cooldown if cooldown
|
|
131
|
+
end
|
|
132
|
+
@circuit_breaker_config || (superclass.respond_to?(:circuit_breaker_config) ? superclass.circuit_breaker_config : nil)
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# Returns the circuit breaker configuration for this agent
|
|
136
|
+
#
|
|
137
|
+
# @return [Hash, nil] The circuit breaker configuration
|
|
138
|
+
def circuit_breaker_config
|
|
139
|
+
@circuit_breaker_config || (superclass.respond_to?(:circuit_breaker_config) ? superclass.circuit_breaker_config : nil)
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# @!endgroup
|
|
143
|
+
|
|
144
|
+
# @!group Parameter DSL
|
|
145
|
+
|
|
146
|
+
# Defines a parameter for the agent
|
|
147
|
+
#
|
|
148
|
+
# Creates an accessor method for the parameter that retrieves values
|
|
149
|
+
# from the options hash, falling back to the default value.
|
|
150
|
+
#
|
|
151
|
+
# @param name [Symbol] The parameter name
|
|
152
|
+
# @param required [Boolean] Whether the parameter is required
|
|
153
|
+
# @param default [Object, nil] Default value if not provided
|
|
154
|
+
# @return [void]
|
|
155
|
+
# @example
|
|
156
|
+
# param :query, required: true
|
|
157
|
+
# param :limit, default: 10
|
|
158
|
+
def param(name, required: false, default: nil)
|
|
159
|
+
@params ||= {}
|
|
160
|
+
@params[name] = { required: required, default: default }
|
|
161
|
+
define_method(name) do
|
|
162
|
+
@options[name] || @options[name.to_s] || self.class.params.dig(name, :default)
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
# Returns all defined parameters including inherited ones
|
|
167
|
+
#
|
|
168
|
+
# @return [Hash{Symbol => Hash}] Parameter definitions
|
|
169
|
+
def params
|
|
170
|
+
parent = superclass.respond_to?(:params) ? superclass.params : {}
|
|
171
|
+
parent.merge(@params || {})
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
# @!endgroup
|
|
175
|
+
|
|
176
|
+
# @!group Caching DSL
|
|
177
|
+
|
|
178
|
+
# Enables caching for this agent with optional TTL
|
|
179
|
+
#
|
|
180
|
+
# @param ttl [ActiveSupport::Duration] Time-to-live for cached responses
|
|
181
|
+
# @return [void]
|
|
182
|
+
# @example
|
|
183
|
+
# cache 1.hour
|
|
184
|
+
def cache(ttl = CACHE_TTL)
|
|
185
|
+
@cache_enabled = true
|
|
186
|
+
@cache_ttl = ttl
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
# Returns whether caching is enabled for this agent
|
|
190
|
+
#
|
|
191
|
+
# @return [Boolean] true if caching is enabled
|
|
192
|
+
def cache_enabled?
|
|
193
|
+
@cache_enabled || false
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
# Returns the cache TTL for this agent
|
|
197
|
+
#
|
|
198
|
+
# @return [ActiveSupport::Duration] The cache TTL
|
|
199
|
+
def cache_ttl
|
|
200
|
+
@cache_ttl || CACHE_TTL
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
# @!endgroup
|
|
204
|
+
|
|
205
|
+
# @!group Streaming DSL
|
|
206
|
+
|
|
207
|
+
# Enables or returns streaming mode for this agent
|
|
208
|
+
#
|
|
209
|
+
# When streaming is enabled and a block is passed to call,
|
|
210
|
+
# chunks will be yielded to the block as they arrive.
|
|
211
|
+
#
|
|
212
|
+
# @param value [Boolean, nil] Whether to enable streaming
|
|
213
|
+
# @return [Boolean] The current streaming setting
|
|
214
|
+
# @example
|
|
215
|
+
# streaming true
|
|
216
|
+
def streaming(value = nil)
|
|
217
|
+
@streaming = value unless value.nil?
|
|
218
|
+
return @streaming unless @streaming.nil?
|
|
219
|
+
|
|
220
|
+
inherited_or_default(:streaming, RubyLLM::Agents.configuration.default_streaming)
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
# @!endgroup
|
|
224
|
+
|
|
225
|
+
# @!group Tools DSL
|
|
226
|
+
|
|
227
|
+
# Sets or returns the tools available to this agent
|
|
228
|
+
#
|
|
229
|
+
# Tools are RubyLLM::Tool classes that the model can invoke.
|
|
230
|
+
# The agent will automatically execute tool calls and continue
|
|
231
|
+
# until the model produces a final response.
|
|
232
|
+
#
|
|
233
|
+
# @param tool_classes [Array<Class>] Tool classes to make available
|
|
234
|
+
# @return [Array<Class>] The current tools
|
|
235
|
+
# @example Single tool
|
|
236
|
+
# tools WeatherTool
|
|
237
|
+
# @example Multiple tools
|
|
238
|
+
# tools WeatherTool, SearchTool, CalculatorTool
|
|
239
|
+
def tools(*tool_classes)
|
|
240
|
+
if tool_classes.any?
|
|
241
|
+
@tools = tool_classes.flatten
|
|
242
|
+
end
|
|
243
|
+
@tools || inherited_or_default(:tools, RubyLLM::Agents.configuration.default_tools)
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
# @!endgroup
|
|
247
|
+
|
|
248
|
+
private
|
|
249
|
+
|
|
250
|
+
# Looks up setting from superclass or uses default
|
|
251
|
+
#
|
|
252
|
+
# @param method [Symbol] The method to call on superclass
|
|
253
|
+
# @param default [Object] Default value if not found
|
|
254
|
+
# @return [Object] The resolved value
|
|
255
|
+
def inherited_or_default(method, default)
|
|
256
|
+
superclass.respond_to?(method) ? superclass.send(method) : default
|
|
257
|
+
end
|
|
258
|
+
end
|
|
259
|
+
end
|
|
260
|
+
end
|
|
261
|
+
end
|