ruby_llm-agents 0.1.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 +7 -0
- data/LICENSE.txt +21 -0
- data/README.md +898 -0
- data/app/channels/ruby_llm/agents/executions_channel.rb +23 -0
- data/app/controllers/ruby_llm/agents/agents_controller.rb +100 -0
- data/app/controllers/ruby_llm/agents/application_controller.rb +20 -0
- data/app/controllers/ruby_llm/agents/dashboard_controller.rb +34 -0
- data/app/controllers/ruby_llm/agents/executions_controller.rb +93 -0
- data/app/helpers/ruby_llm/agents/application_helper.rb +149 -0
- data/app/javascript/ruby_llm/agents/controllers/filter_controller.js +56 -0
- data/app/javascript/ruby_llm/agents/controllers/index.js +12 -0
- data/app/javascript/ruby_llm/agents/controllers/refresh_controller.js +83 -0
- data/app/models/ruby_llm/agents/execution/analytics.rb +166 -0
- data/app/models/ruby_llm/agents/execution/metrics.rb +89 -0
- data/app/models/ruby_llm/agents/execution/scopes.rb +81 -0
- data/app/models/ruby_llm/agents/execution.rb +81 -0
- data/app/services/ruby_llm/agents/agent_registry.rb +112 -0
- data/app/views/layouts/rubyllm/agents/application.html.erb +276 -0
- data/app/views/rubyllm/agents/agents/index.html.erb +89 -0
- data/app/views/rubyllm/agents/agents/show.html.erb +562 -0
- data/app/views/rubyllm/agents/dashboard/_execution_item.html.erb +48 -0
- data/app/views/rubyllm/agents/dashboard/index.html.erb +121 -0
- data/app/views/rubyllm/agents/executions/_execution.html.erb +64 -0
- data/app/views/rubyllm/agents/executions/_filters.html.erb +172 -0
- data/app/views/rubyllm/agents/executions/_list.html.erb +229 -0
- data/app/views/rubyllm/agents/executions/index.html.erb +83 -0
- data/app/views/rubyllm/agents/executions/index.turbo_stream.erb +4 -0
- data/app/views/rubyllm/agents/executions/show.html.erb +240 -0
- data/app/views/rubyllm/agents/shared/_executions_table.html.erb +193 -0
- data/app/views/rubyllm/agents/shared/_stat_card.html.erb +14 -0
- data/app/views/rubyllm/agents/shared/_status_badge.html.erb +65 -0
- data/app/views/rubyllm/agents/shared/_status_dot.html.erb +18 -0
- data/config/routes.rb +13 -0
- data/lib/generators/ruby_llm_agents/agent_generator.rb +79 -0
- data/lib/generators/ruby_llm_agents/install_generator.rb +89 -0
- data/lib/generators/ruby_llm_agents/templates/add_prompts_migration.rb.tt +12 -0
- data/lib/generators/ruby_llm_agents/templates/agent.rb.tt +46 -0
- data/lib/generators/ruby_llm_agents/templates/application_agent.rb.tt +22 -0
- data/lib/generators/ruby_llm_agents/templates/initializer.rb.tt +36 -0
- data/lib/generators/ruby_llm_agents/templates/migration.rb.tt +66 -0
- data/lib/generators/ruby_llm_agents/upgrade_generator.rb +59 -0
- data/lib/ruby_llm/agents/base.rb +271 -0
- data/lib/ruby_llm/agents/configuration.rb +36 -0
- data/lib/ruby_llm/agents/engine.rb +32 -0
- data/lib/ruby_llm/agents/execution_logger_job.rb +59 -0
- data/lib/ruby_llm/agents/inflections.rb +13 -0
- data/lib/ruby_llm/agents/instrumentation.rb +245 -0
- data/lib/ruby_llm/agents/version.rb +7 -0
- data/lib/ruby_llm/agents.rb +26 -0
- data/lib/ruby_llm-agents.rb +3 -0
- metadata +164 -0
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
module Agents
|
|
5
|
+
class Execution
|
|
6
|
+
# Analytics concern for advanced reporting and analysis
|
|
7
|
+
#
|
|
8
|
+
# Provides class methods for:
|
|
9
|
+
# - Daily reports with key metrics
|
|
10
|
+
# - Cost breakdown by agent type
|
|
11
|
+
# - Performance stats for specific agents
|
|
12
|
+
# - Version comparison
|
|
13
|
+
# - Trend analysis over time
|
|
14
|
+
#
|
|
15
|
+
module Analytics
|
|
16
|
+
extend ActiveSupport::Concern
|
|
17
|
+
|
|
18
|
+
class_methods do
|
|
19
|
+
# Daily report with key metrics
|
|
20
|
+
def daily_report
|
|
21
|
+
scope = today
|
|
22
|
+
|
|
23
|
+
{
|
|
24
|
+
date: Date.current,
|
|
25
|
+
total_executions: scope.count,
|
|
26
|
+
successful: scope.successful.count,
|
|
27
|
+
failed: scope.failed.count,
|
|
28
|
+
total_cost: scope.total_cost_sum || 0,
|
|
29
|
+
total_tokens: scope.total_tokens_sum || 0,
|
|
30
|
+
avg_duration_ms: scope.avg_duration&.round || 0,
|
|
31
|
+
avg_tokens: scope.avg_tokens&.round || 0,
|
|
32
|
+
error_rate: calculate_error_rate(scope),
|
|
33
|
+
by_agent: scope.group(:agent_type).count,
|
|
34
|
+
top_errors: scope.errors.group(:error_class).count.sort_by { |_, v| -v }.first(5).to_h
|
|
35
|
+
}
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Cost breakdown by agent type
|
|
39
|
+
def cost_by_agent(period: :today)
|
|
40
|
+
public_send(period)
|
|
41
|
+
.group(:agent_type)
|
|
42
|
+
.sum(:total_cost)
|
|
43
|
+
.sort_by { |_, cost| -(cost || 0) }
|
|
44
|
+
.to_h
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Performance stats for specific agent
|
|
48
|
+
def stats_for(agent_type, period: :today)
|
|
49
|
+
scope = by_agent(agent_type).public_send(period)
|
|
50
|
+
count = scope.count
|
|
51
|
+
total_cost = scope.total_cost_sum || 0
|
|
52
|
+
|
|
53
|
+
{
|
|
54
|
+
agent_type: agent_type,
|
|
55
|
+
period: period,
|
|
56
|
+
count: count,
|
|
57
|
+
total_cost: total_cost,
|
|
58
|
+
avg_cost: count > 0 ? (total_cost / count).round(6) : 0,
|
|
59
|
+
total_tokens: scope.total_tokens_sum || 0,
|
|
60
|
+
avg_tokens: scope.avg_tokens&.round || 0,
|
|
61
|
+
avg_duration_ms: scope.avg_duration&.round || 0,
|
|
62
|
+
success_rate: calculate_success_rate(scope),
|
|
63
|
+
error_rate: calculate_error_rate(scope)
|
|
64
|
+
}
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Compare versions of the same agent
|
|
68
|
+
def compare_versions(agent_type, version1, version2, period: :this_week)
|
|
69
|
+
base_scope = by_agent(agent_type).public_send(period)
|
|
70
|
+
|
|
71
|
+
v1_stats = stats_for_scope(base_scope.by_version(version1))
|
|
72
|
+
v2_stats = stats_for_scope(base_scope.by_version(version2))
|
|
73
|
+
|
|
74
|
+
{
|
|
75
|
+
agent_type: agent_type,
|
|
76
|
+
period: period,
|
|
77
|
+
version1: { version: version1, **v1_stats },
|
|
78
|
+
version2: { version: version2, **v2_stats },
|
|
79
|
+
improvements: {
|
|
80
|
+
cost_change_pct: percent_change(v1_stats[:avg_cost], v2_stats[:avg_cost]),
|
|
81
|
+
token_change_pct: percent_change(v1_stats[:avg_tokens], v2_stats[:avg_tokens]),
|
|
82
|
+
speed_change_pct: percent_change(v1_stats[:avg_duration_ms], v2_stats[:avg_duration_ms])
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Trend analysis over time
|
|
88
|
+
def trend_analysis(agent_type: nil, days: 7)
|
|
89
|
+
scope = agent_type ? by_agent(agent_type) : all
|
|
90
|
+
|
|
91
|
+
(0...days).map do |days_ago|
|
|
92
|
+
date = days_ago.days.ago.to_date
|
|
93
|
+
day_scope = scope.where(created_at: date.beginning_of_day..date.end_of_day)
|
|
94
|
+
|
|
95
|
+
{
|
|
96
|
+
date: date,
|
|
97
|
+
count: day_scope.count,
|
|
98
|
+
total_cost: day_scope.total_cost_sum || 0,
|
|
99
|
+
avg_duration_ms: day_scope.avg_duration&.round || 0,
|
|
100
|
+
error_count: day_scope.failed.count
|
|
101
|
+
}
|
|
102
|
+
end.reverse
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# Chart data: Hourly activity chart for today showing success/failed
|
|
106
|
+
def hourly_activity_chart
|
|
107
|
+
success_data = {}
|
|
108
|
+
failed_data = {}
|
|
109
|
+
|
|
110
|
+
# Create entries for each hour of the day (0-23)
|
|
111
|
+
(0..23).each do |hour|
|
|
112
|
+
time_label = format("%02d:00", hour)
|
|
113
|
+
start_time = Time.current.beginning_of_day + hour.hours
|
|
114
|
+
end_time = start_time + 1.hour
|
|
115
|
+
|
|
116
|
+
hour_scope = where(created_at: start_time...end_time)
|
|
117
|
+
total = hour_scope.count
|
|
118
|
+
failed = hour_scope.failed.count
|
|
119
|
+
|
|
120
|
+
success_data[time_label] = total - failed
|
|
121
|
+
failed_data[time_label] = failed
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
[
|
|
125
|
+
{ name: "Success", data: success_data },
|
|
126
|
+
{ name: "Failed", data: failed_data }
|
|
127
|
+
]
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
private
|
|
131
|
+
|
|
132
|
+
def calculate_success_rate(scope)
|
|
133
|
+
total = scope.count
|
|
134
|
+
return 0.0 if total.zero?
|
|
135
|
+
(scope.successful.count.to_f / total * 100).round(2)
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def calculate_error_rate(scope)
|
|
139
|
+
total = scope.count
|
|
140
|
+
return 0.0 if total.zero?
|
|
141
|
+
(scope.failed.count.to_f / total * 100).round(2)
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def stats_for_scope(scope)
|
|
145
|
+
count = scope.count
|
|
146
|
+
total_cost = scope.total_cost_sum || 0
|
|
147
|
+
|
|
148
|
+
{
|
|
149
|
+
count: count,
|
|
150
|
+
total_cost: total_cost,
|
|
151
|
+
avg_cost: count > 0 ? (total_cost / count).round(6) : 0,
|
|
152
|
+
avg_tokens: scope.avg_tokens&.round || 0,
|
|
153
|
+
avg_duration_ms: scope.avg_duration&.round || 0,
|
|
154
|
+
success_rate: calculate_success_rate(scope)
|
|
155
|
+
}
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def percent_change(old_value, new_value)
|
|
159
|
+
return 0.0 if old_value.nil? || old_value.zero?
|
|
160
|
+
((new_value - old_value).to_f / old_value * 100).round(2)
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
end
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
module Agents
|
|
5
|
+
class Execution
|
|
6
|
+
# Metrics concern for cost calculations and performance metrics
|
|
7
|
+
#
|
|
8
|
+
# Provides methods for:
|
|
9
|
+
# - Calculating costs from token usage via RubyLLM pricing
|
|
10
|
+
# - Human-readable duration formatting
|
|
11
|
+
# - Performance metrics (tokens/second, cost per 1K tokens)
|
|
12
|
+
# - Formatted cost display helpers
|
|
13
|
+
#
|
|
14
|
+
module Metrics
|
|
15
|
+
extend ActiveSupport::Concern
|
|
16
|
+
|
|
17
|
+
# Calculate costs from token usage and model pricing
|
|
18
|
+
def calculate_costs!(model_info = nil)
|
|
19
|
+
return unless input_tokens && output_tokens
|
|
20
|
+
|
|
21
|
+
model_info ||= resolve_model_info
|
|
22
|
+
return unless model_info
|
|
23
|
+
|
|
24
|
+
# Get pricing from RubyLLM (prices per million tokens in dollars)
|
|
25
|
+
input_price_per_million = model_info.pricing&.text_tokens&.input || 0
|
|
26
|
+
output_price_per_million = model_info.pricing&.text_tokens&.output || 0
|
|
27
|
+
|
|
28
|
+
# Calculate costs in dollars (with 6 decimal precision for micro-dollars)
|
|
29
|
+
self.input_cost = ((input_tokens / 1_000_000.0) * input_price_per_million).round(6)
|
|
30
|
+
self.output_cost = ((output_tokens / 1_000_000.0) * output_price_per_million).round(6)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Human-readable duration
|
|
34
|
+
def duration_seconds
|
|
35
|
+
duration_ms ? (duration_ms / 1000.0).round(2) : nil
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Tokens per second
|
|
39
|
+
def tokens_per_second
|
|
40
|
+
return nil unless duration_ms && duration_ms > 0 && total_tokens
|
|
41
|
+
(total_tokens / duration_seconds.to_f).round(2)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Cost per 1K tokens (for comparison, in dollars)
|
|
45
|
+
def cost_per_1k_tokens
|
|
46
|
+
return nil unless total_tokens && total_tokens > 0 && total_cost
|
|
47
|
+
(total_cost / total_tokens.to_f * 1000).round(6)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# ==============================================================================
|
|
51
|
+
# Cost Display Helpers
|
|
52
|
+
# ==============================================================================
|
|
53
|
+
#
|
|
54
|
+
# Format cost as currency string
|
|
55
|
+
# Example: format_cost(0.000045) => "$0.000045"
|
|
56
|
+
#
|
|
57
|
+
|
|
58
|
+
def formatted_input_cost
|
|
59
|
+
format_cost(input_cost)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def formatted_output_cost
|
|
63
|
+
format_cost(output_cost)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def formatted_total_cost
|
|
67
|
+
format_cost(total_cost)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
private
|
|
71
|
+
|
|
72
|
+
def resolve_model_info
|
|
73
|
+
return nil unless model_id
|
|
74
|
+
|
|
75
|
+
model, _provider = RubyLLM::Models.resolve(model_id)
|
|
76
|
+
model
|
|
77
|
+
rescue RubyLLM::ModelNotFoundError
|
|
78
|
+
Rails.logger.warn("[RubyLLM::Agents] Model not found for pricing: #{model_id}")
|
|
79
|
+
nil
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def format_cost(cost)
|
|
83
|
+
return nil unless cost
|
|
84
|
+
format("$%.6f", cost)
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
module Agents
|
|
5
|
+
class Execution
|
|
6
|
+
# Scopes concern for common query patterns
|
|
7
|
+
#
|
|
8
|
+
# Provides chainable scopes for:
|
|
9
|
+
# - Time-based filtering (today, this_week, last_n_days)
|
|
10
|
+
# - Agent-based filtering (by_agent, by_version, by_model)
|
|
11
|
+
# - Status filtering (successful, failed, errors, timeouts)
|
|
12
|
+
# - Performance filtering (expensive, slow, high_token)
|
|
13
|
+
# - JSONB parameter queries
|
|
14
|
+
# - Aggregations (total_cost_sum, avg_duration)
|
|
15
|
+
#
|
|
16
|
+
module Scopes
|
|
17
|
+
extend ActiveSupport::Concern
|
|
18
|
+
|
|
19
|
+
included do
|
|
20
|
+
# Time-based scopes
|
|
21
|
+
scope :recent, ->(limit = 100) { order(created_at: :desc).limit(limit) }
|
|
22
|
+
scope :oldest, ->(limit = 100) { order(created_at: :asc).limit(limit) }
|
|
23
|
+
scope :all_time, -> { all } # Explicit scope for all-time queries (used by analytics)
|
|
24
|
+
scope :today, -> { where("created_at >= ?", Time.current.beginning_of_day) }
|
|
25
|
+
scope :yesterday, -> { where(created_at: 1.day.ago.beginning_of_day..1.day.ago.end_of_day) }
|
|
26
|
+
scope :this_week, -> { where("created_at >= ?", Time.current.beginning_of_week) }
|
|
27
|
+
scope :this_month, -> { where("created_at >= ?", Time.current.beginning_of_month) }
|
|
28
|
+
scope :last_n_days, ->(n) { where("created_at >= ?", n.days.ago) }
|
|
29
|
+
|
|
30
|
+
# Agent-based scopes
|
|
31
|
+
scope :by_agent, ->(agent_type) { where(agent_type: agent_type.to_s) }
|
|
32
|
+
scope :by_version, ->(version) { where(agent_version: version.to_s) }
|
|
33
|
+
scope :by_model, ->(model_id) { where(model_id: model_id.to_s) }
|
|
34
|
+
|
|
35
|
+
# Status scopes
|
|
36
|
+
scope :running, -> { where(status: "running") }
|
|
37
|
+
scope :in_progress, -> { running } # alias
|
|
38
|
+
scope :completed, -> { where.not(status: "running") }
|
|
39
|
+
scope :successful, -> { where(status: "success") }
|
|
40
|
+
scope :failed, -> { where(status: %w[error timeout]) }
|
|
41
|
+
scope :errors, -> { where(status: "error") }
|
|
42
|
+
scope :timeouts, -> { where(status: "timeout") }
|
|
43
|
+
|
|
44
|
+
# Performance scopes
|
|
45
|
+
scope :expensive, ->(threshold_dollars = 1.00) { where("total_cost >= ?", threshold_dollars) }
|
|
46
|
+
scope :slow, ->(threshold_ms = 5000) { where("duration_ms >= ?", threshold_ms) }
|
|
47
|
+
scope :high_token, ->(threshold = 10_000) { where("total_tokens >= ?", threshold) }
|
|
48
|
+
|
|
49
|
+
# Parameter-based scopes (JSONB queries)
|
|
50
|
+
scope :with_parameter, ->(key, value = nil) do
|
|
51
|
+
if value
|
|
52
|
+
where("parameters @> ?", { key => value }.to_json)
|
|
53
|
+
else
|
|
54
|
+
where("parameters ? :key", key: key.to_s)
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Aggregation methods (not scopes - these return values, not relations)
|
|
61
|
+
class_methods do
|
|
62
|
+
def total_cost_sum
|
|
63
|
+
sum(:total_cost)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def total_tokens_sum
|
|
67
|
+
sum(:total_tokens)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def avg_duration
|
|
71
|
+
average(:duration_ms)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def avg_tokens
|
|
75
|
+
average(:total_tokens)
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
module Agents
|
|
5
|
+
# Execution model for tracking agent executions
|
|
6
|
+
#
|
|
7
|
+
# Stores all agent execution data including:
|
|
8
|
+
# - Agent identification (type, version)
|
|
9
|
+
# - Model configuration (model_id, temperature)
|
|
10
|
+
# - Timing (started_at, completed_at, duration_ms)
|
|
11
|
+
# - Token usage (input_tokens, output_tokens, cached_tokens)
|
|
12
|
+
# - Costs (input_cost, output_cost, total_cost in dollars)
|
|
13
|
+
# - Status (success, error, timeout)
|
|
14
|
+
# - Parameters and metadata (JSONB)
|
|
15
|
+
# - Error tracking (error_class, error_message)
|
|
16
|
+
#
|
|
17
|
+
class Execution < ::ActiveRecord::Base
|
|
18
|
+
self.table_name = "ruby_llm_agents_executions"
|
|
19
|
+
|
|
20
|
+
include Execution::Metrics
|
|
21
|
+
include Execution::Scopes
|
|
22
|
+
include Execution::Analytics
|
|
23
|
+
|
|
24
|
+
# Status enum
|
|
25
|
+
# - running: execution in progress
|
|
26
|
+
# - success: completed successfully
|
|
27
|
+
# - error: completed with error
|
|
28
|
+
# - timeout: completed due to timeout
|
|
29
|
+
enum :status, %w[running success error timeout].index_by(&:itself), prefix: true
|
|
30
|
+
|
|
31
|
+
# Validations
|
|
32
|
+
validates :agent_type, :model_id, :started_at, presence: true
|
|
33
|
+
validates :status, inclusion: { in: statuses.keys }
|
|
34
|
+
validates :agent_version, presence: true
|
|
35
|
+
validates :temperature, numericality: { greater_than_or_equal_to: 0, less_than_or_equal_to: 2 }, allow_nil: true
|
|
36
|
+
validates :input_tokens, :output_tokens, numericality: { greater_than_or_equal_to: 0 }, allow_nil: true
|
|
37
|
+
validates :duration_ms, numericality: { greater_than_or_equal_to: 0 }, allow_nil: true
|
|
38
|
+
validates :input_cost, :output_cost, :total_cost, numericality: { greater_than_or_equal_to: 0 }, allow_nil: true
|
|
39
|
+
|
|
40
|
+
# Callbacks
|
|
41
|
+
before_save :calculate_total_tokens, if: -> { input_tokens_changed? || output_tokens_changed? }
|
|
42
|
+
before_save :calculate_total_cost, if: -> { input_cost_changed? || output_cost_changed? }
|
|
43
|
+
after_commit :broadcast_execution, on: %i[create update]
|
|
44
|
+
|
|
45
|
+
# Broadcast execution changes via ActionCable
|
|
46
|
+
def broadcast_execution
|
|
47
|
+
ActionCable.server.broadcast(
|
|
48
|
+
"ruby_llm_agents:executions",
|
|
49
|
+
{
|
|
50
|
+
action: previously_new_record? ? "created" : "updated",
|
|
51
|
+
id: id,
|
|
52
|
+
status: status,
|
|
53
|
+
html: render_execution_html
|
|
54
|
+
}
|
|
55
|
+
)
|
|
56
|
+
rescue StandardError => e
|
|
57
|
+
Rails.logger.error("[RubyLLM::Agents] Failed to broadcast execution: #{e.message}")
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
private
|
|
61
|
+
|
|
62
|
+
def render_execution_html
|
|
63
|
+
ApplicationController.render(
|
|
64
|
+
partial: "rubyllm/agents/dashboard/execution_item",
|
|
65
|
+
locals: { execution: self }
|
|
66
|
+
)
|
|
67
|
+
rescue StandardError
|
|
68
|
+
# Partial may not exist in all contexts
|
|
69
|
+
nil
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def calculate_total_tokens
|
|
73
|
+
self.total_tokens = (input_tokens || 0) + (output_tokens || 0)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def calculate_total_cost
|
|
77
|
+
self.total_cost = (input_cost || 0) + (output_cost || 0)
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
module Agents
|
|
5
|
+
# Service for discovering and listing available agents
|
|
6
|
+
#
|
|
7
|
+
# Combines two sources:
|
|
8
|
+
# 1. File system - Classes inheriting from ApplicationAgent in app/agents/
|
|
9
|
+
# 2. Execution history - Agent types that have execution records
|
|
10
|
+
#
|
|
11
|
+
# This ensures all agents are visible, including:
|
|
12
|
+
# - Agents that have never been executed
|
|
13
|
+
# - Deleted agents that still have execution history
|
|
14
|
+
#
|
|
15
|
+
class AgentRegistry
|
|
16
|
+
class << self
|
|
17
|
+
# Returns all unique agent type names (sorted)
|
|
18
|
+
def all
|
|
19
|
+
(file_system_agents + execution_agents).uniq.sort
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Returns agent class if it exists, nil if only in execution history
|
|
23
|
+
def find(agent_type)
|
|
24
|
+
agent_type.safe_constantize
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Check if an agent class is currently defined
|
|
28
|
+
def exists?(agent_type)
|
|
29
|
+
find(agent_type).present?
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Get detailed info about all agents
|
|
33
|
+
def all_with_details
|
|
34
|
+
all.map do |agent_type|
|
|
35
|
+
build_agent_info(agent_type)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
private
|
|
40
|
+
|
|
41
|
+
# Find agent classes defined in the file system
|
|
42
|
+
def file_system_agents
|
|
43
|
+
# Ensure all agent classes are loaded
|
|
44
|
+
eager_load_agents!
|
|
45
|
+
|
|
46
|
+
# Find all descendants of the base class
|
|
47
|
+
base_class = RubyLLM::Agents::Base
|
|
48
|
+
base_class.descendants.map(&:name).compact
|
|
49
|
+
rescue StandardError => e
|
|
50
|
+
Rails.logger.error("[RubyLLM::Agents] Error loading agents from file system: #{e.message}")
|
|
51
|
+
[]
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Find agent types from execution history
|
|
55
|
+
def execution_agents
|
|
56
|
+
Execution.distinct.pluck(:agent_type).compact
|
|
57
|
+
rescue StandardError => e
|
|
58
|
+
Rails.logger.error("[RubyLLM::Agents] Error loading agents from executions: #{e.message}")
|
|
59
|
+
[]
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Eager load all agent files to ensure descendants are registered
|
|
63
|
+
def eager_load_agents!
|
|
64
|
+
agents_path = Rails.root.join("app", "agents")
|
|
65
|
+
return unless agents_path.exist?
|
|
66
|
+
|
|
67
|
+
Dir.glob(agents_path.join("**", "*.rb")).each do |file|
|
|
68
|
+
require_dependency file
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Build detailed info hash for an agent
|
|
73
|
+
def build_agent_info(agent_type)
|
|
74
|
+
agent_class = find(agent_type)
|
|
75
|
+
stats = fetch_stats(agent_type)
|
|
76
|
+
|
|
77
|
+
{
|
|
78
|
+
name: agent_type,
|
|
79
|
+
class: agent_class,
|
|
80
|
+
active: agent_class.present?,
|
|
81
|
+
version: agent_class&.version || "N/A",
|
|
82
|
+
model: agent_class&.model || "N/A",
|
|
83
|
+
temperature: agent_class&.temperature,
|
|
84
|
+
timeout: agent_class&.timeout,
|
|
85
|
+
cache_enabled: agent_class&.cache_enabled? || false,
|
|
86
|
+
cache_ttl: agent_class&.cache_ttl,
|
|
87
|
+
params: agent_class&.params || {},
|
|
88
|
+
execution_count: stats[:count],
|
|
89
|
+
total_cost: stats[:total_cost],
|
|
90
|
+
total_tokens: stats[:total_tokens],
|
|
91
|
+
avg_duration_ms: stats[:avg_duration_ms],
|
|
92
|
+
success_rate: stats[:success_rate],
|
|
93
|
+
error_rate: stats[:error_rate],
|
|
94
|
+
last_executed: last_execution_time(agent_type)
|
|
95
|
+
}
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def fetch_stats(agent_type)
|
|
99
|
+
Execution.stats_for(agent_type, period: :all_time)
|
|
100
|
+
rescue StandardError
|
|
101
|
+
{ count: 0, total_cost: 0, total_tokens: 0, avg_duration_ms: 0, success_rate: 0, error_rate: 0 }
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def last_execution_time(agent_type)
|
|
105
|
+
Execution.by_agent(agent_type).order(created_at: :desc).first&.created_at
|
|
106
|
+
rescue StandardError
|
|
107
|
+
nil
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|