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.
Files changed (51) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE.txt +21 -0
  3. data/README.md +898 -0
  4. data/app/channels/ruby_llm/agents/executions_channel.rb +23 -0
  5. data/app/controllers/ruby_llm/agents/agents_controller.rb +100 -0
  6. data/app/controllers/ruby_llm/agents/application_controller.rb +20 -0
  7. data/app/controllers/ruby_llm/agents/dashboard_controller.rb +34 -0
  8. data/app/controllers/ruby_llm/agents/executions_controller.rb +93 -0
  9. data/app/helpers/ruby_llm/agents/application_helper.rb +149 -0
  10. data/app/javascript/ruby_llm/agents/controllers/filter_controller.js +56 -0
  11. data/app/javascript/ruby_llm/agents/controllers/index.js +12 -0
  12. data/app/javascript/ruby_llm/agents/controllers/refresh_controller.js +83 -0
  13. data/app/models/ruby_llm/agents/execution/analytics.rb +166 -0
  14. data/app/models/ruby_llm/agents/execution/metrics.rb +89 -0
  15. data/app/models/ruby_llm/agents/execution/scopes.rb +81 -0
  16. data/app/models/ruby_llm/agents/execution.rb +81 -0
  17. data/app/services/ruby_llm/agents/agent_registry.rb +112 -0
  18. data/app/views/layouts/rubyllm/agents/application.html.erb +276 -0
  19. data/app/views/rubyllm/agents/agents/index.html.erb +89 -0
  20. data/app/views/rubyllm/agents/agents/show.html.erb +562 -0
  21. data/app/views/rubyllm/agents/dashboard/_execution_item.html.erb +48 -0
  22. data/app/views/rubyllm/agents/dashboard/index.html.erb +121 -0
  23. data/app/views/rubyllm/agents/executions/_execution.html.erb +64 -0
  24. data/app/views/rubyllm/agents/executions/_filters.html.erb +172 -0
  25. data/app/views/rubyllm/agents/executions/_list.html.erb +229 -0
  26. data/app/views/rubyllm/agents/executions/index.html.erb +83 -0
  27. data/app/views/rubyllm/agents/executions/index.turbo_stream.erb +4 -0
  28. data/app/views/rubyllm/agents/executions/show.html.erb +240 -0
  29. data/app/views/rubyllm/agents/shared/_executions_table.html.erb +193 -0
  30. data/app/views/rubyllm/agents/shared/_stat_card.html.erb +14 -0
  31. data/app/views/rubyllm/agents/shared/_status_badge.html.erb +65 -0
  32. data/app/views/rubyllm/agents/shared/_status_dot.html.erb +18 -0
  33. data/config/routes.rb +13 -0
  34. data/lib/generators/ruby_llm_agents/agent_generator.rb +79 -0
  35. data/lib/generators/ruby_llm_agents/install_generator.rb +89 -0
  36. data/lib/generators/ruby_llm_agents/templates/add_prompts_migration.rb.tt +12 -0
  37. data/lib/generators/ruby_llm_agents/templates/agent.rb.tt +46 -0
  38. data/lib/generators/ruby_llm_agents/templates/application_agent.rb.tt +22 -0
  39. data/lib/generators/ruby_llm_agents/templates/initializer.rb.tt +36 -0
  40. data/lib/generators/ruby_llm_agents/templates/migration.rb.tt +66 -0
  41. data/lib/generators/ruby_llm_agents/upgrade_generator.rb +59 -0
  42. data/lib/ruby_llm/agents/base.rb +271 -0
  43. data/lib/ruby_llm/agents/configuration.rb +36 -0
  44. data/lib/ruby_llm/agents/engine.rb +32 -0
  45. data/lib/ruby_llm/agents/execution_logger_job.rb +59 -0
  46. data/lib/ruby_llm/agents/inflections.rb +13 -0
  47. data/lib/ruby_llm/agents/instrumentation.rb +245 -0
  48. data/lib/ruby_llm/agents/version.rb +7 -0
  49. data/lib/ruby_llm/agents.rb +26 -0
  50. data/lib/ruby_llm-agents.rb +3 -0
  51. 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