ruby_llm-agents 0.2.3 → 0.3.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 (62) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +273 -0
  3. data/app/channels/ruby_llm/agents/executions_channel.rb +24 -1
  4. data/app/controllers/concerns/ruby_llm/agents/filterable.rb +81 -0
  5. data/app/controllers/concerns/ruby_llm/agents/paginatable.rb +51 -0
  6. data/app/controllers/ruby_llm/agents/agents_controller.rb +228 -59
  7. data/app/controllers/ruby_llm/agents/dashboard_controller.rb +167 -12
  8. data/app/controllers/ruby_llm/agents/executions_controller.rb +189 -31
  9. data/app/controllers/ruby_llm/agents/settings_controller.rb +20 -0
  10. data/app/helpers/ruby_llm/agents/application_helper.rb +307 -7
  11. data/app/models/ruby_llm/agents/execution/analytics.rb +224 -20
  12. data/app/models/ruby_llm/agents/execution/metrics.rb +41 -25
  13. data/app/models/ruby_llm/agents/execution/scopes.rb +234 -14
  14. data/app/models/ruby_llm/agents/execution.rb +259 -16
  15. data/app/services/ruby_llm/agents/agent_registry.rb +49 -12
  16. data/app/views/layouts/rubyllm/agents/application.html.erb +351 -85
  17. data/app/views/rubyllm/agents/agents/_version_comparison.html.erb +186 -0
  18. data/app/views/rubyllm/agents/agents/show.html.erb +233 -10
  19. data/app/views/rubyllm/agents/dashboard/_action_center.html.erb +62 -0
  20. data/app/views/rubyllm/agents/dashboard/_alerts_feed.html.erb +62 -0
  21. data/app/views/rubyllm/agents/dashboard/_breaker_strip.html.erb +47 -0
  22. data/app/views/rubyllm/agents/dashboard/_budgets_bar.html.erb +165 -0
  23. data/app/views/rubyllm/agents/dashboard/_now_strip.html.erb +10 -0
  24. data/app/views/rubyllm/agents/dashboard/_now_strip_values.html.erb +71 -0
  25. data/app/views/rubyllm/agents/dashboard/index.html.erb +215 -109
  26. data/app/views/rubyllm/agents/executions/_filters.html.erb +152 -155
  27. data/app/views/rubyllm/agents/executions/_list.html.erb +103 -12
  28. data/app/views/rubyllm/agents/executions/dry_run.html.erb +149 -0
  29. data/app/views/rubyllm/agents/executions/index.html.erb +17 -72
  30. data/app/views/rubyllm/agents/executions/index.turbo_stream.erb +16 -2
  31. data/app/views/rubyllm/agents/executions/show.html.erb +693 -14
  32. data/app/views/rubyllm/agents/settings/show.html.erb +369 -0
  33. data/app/views/rubyllm/agents/shared/_filter_dropdown.html.erb +121 -0
  34. data/app/views/rubyllm/agents/shared/_select_dropdown.html.erb +85 -0
  35. data/config/routes.rb +7 -0
  36. data/lib/generators/ruby_llm_agents/templates/add_attempts_migration.rb.tt +27 -0
  37. data/lib/generators/ruby_llm_agents/templates/add_caching_migration.rb.tt +23 -0
  38. data/lib/generators/ruby_llm_agents/templates/add_finish_reason_migration.rb.tt +19 -0
  39. data/lib/generators/ruby_llm_agents/templates/add_routing_migration.rb.tt +19 -0
  40. data/lib/generators/ruby_llm_agents/templates/add_streaming_migration.rb.tt +8 -0
  41. data/lib/generators/ruby_llm_agents/templates/add_tracing_migration.rb.tt +34 -0
  42. data/lib/generators/ruby_llm_agents/templates/agent.rb.tt +66 -4
  43. data/lib/generators/ruby_llm_agents/templates/application_agent.rb.tt +53 -6
  44. data/lib/generators/ruby_llm_agents/templates/initializer.rb.tt +139 -8
  45. data/lib/generators/ruby_llm_agents/templates/migration.rb.tt +38 -1
  46. data/lib/generators/ruby_llm_agents/upgrade_generator.rb +78 -0
  47. data/lib/ruby_llm/agents/alert_manager.rb +207 -0
  48. data/lib/ruby_llm/agents/attempt_tracker.rb +295 -0
  49. data/lib/ruby_llm/agents/base.rb +580 -112
  50. data/lib/ruby_llm/agents/budget_tracker.rb +360 -0
  51. data/lib/ruby_llm/agents/circuit_breaker.rb +197 -0
  52. data/lib/ruby_llm/agents/configuration.rb +279 -1
  53. data/lib/ruby_llm/agents/engine.rb +59 -6
  54. data/lib/ruby_llm/agents/execution_logger_job.rb +17 -6
  55. data/lib/ruby_llm/agents/inflections.rb +13 -2
  56. data/lib/ruby_llm/agents/instrumentation.rb +538 -87
  57. data/lib/ruby_llm/agents/redactor.rb +130 -0
  58. data/lib/ruby_llm/agents/reliability.rb +185 -0
  59. data/lib/ruby_llm/agents/version.rb +3 -1
  60. data/lib/ruby_llm/agents.rb +52 -0
  61. metadata +41 -2
  62. data/app/controllers/ruby_llm/agents/application_controller.rb +0 -37
@@ -2,12 +2,28 @@
2
2
 
3
3
  module RubyLLM
4
4
  module Agents
5
+ # Controller for browsing and searching execution records
6
+ #
7
+ # Provides paginated listing, filtering, and detail views for all
8
+ # agent executions. Supports both HTML and Turbo Stream responses
9
+ # for seamless filtering without full page reloads.
10
+ #
11
+ # @see Paginatable For pagination implementation
12
+ # @see Filterable For filter parsing and validation
13
+ # @api private
5
14
  class ExecutionsController < ApplicationController
15
+ include Paginatable
16
+ include Filterable
17
+
18
+ CSV_COLUMNS = %w[id agent_type agent_version status model_id total_tokens total_cost
19
+ duration_ms created_at error_class error_message].freeze
20
+
21
+ # Lists all executions with filtering and pagination
22
+ #
23
+ # @return [void]
6
24
  def index
7
- @agent_types = Execution.distinct.pluck(:agent_type)
8
- @statuses = Execution.statuses.keys
9
- load_paginated_executions
10
- load_filter_stats
25
+ load_filter_options
26
+ load_executions_with_stats
11
27
 
12
28
  respond_to do |format|
13
29
  format.html
@@ -15,15 +31,22 @@ module RubyLLM
15
31
  end
16
32
  end
17
33
 
34
+ # Shows a single execution's details
35
+ #
36
+ # @return [void]
18
37
  def show
19
38
  @execution = Execution.find(params[:id])
20
39
  end
21
40
 
41
+ # Handles filter search requests via Turbo Stream
42
+ #
43
+ # Returns the same data as index but optimized for AJAX/Turbo
44
+ # requests, replacing only the executions list partial.
45
+ #
46
+ # @return [void]
22
47
  def search
23
- @agent_types = Execution.distinct.pluck(:agent_type)
24
- @statuses = Execution.statuses.keys
25
- load_paginated_executions
26
- load_filter_stats
48
+ load_filter_options
49
+ load_executions_with_stats
27
50
 
28
51
  respond_to do |format|
29
52
  format.html { render :index }
@@ -37,25 +60,145 @@ module RubyLLM
37
60
  end
38
61
  end
39
62
 
63
+ # Reruns an execution with the same parameters
64
+ #
65
+ # Supports both dry-run mode (returns prompt info without API call)
66
+ # and real reruns that create a new execution.
67
+ #
68
+ # @return [void]
69
+ def rerun
70
+ @execution = Execution.find(params[:id])
71
+ dry_run = params[:dry_run] == "true"
72
+
73
+ agent_class = AgentRegistry.find(@execution.agent_type)
74
+
75
+ unless agent_class
76
+ flash[:alert] = "Agent class '#{@execution.agent_type}' not found. Cannot rerun."
77
+ redirect_to execution_path(@execution)
78
+ return
79
+ end
80
+
81
+ # Prepare parameters from original execution
82
+ original_params = @execution.parameters&.symbolize_keys || {}
83
+
84
+ if dry_run
85
+ # Dry run mode - show what would be sent without making API call
86
+ result = agent_class.call(**original_params, dry_run: true)
87
+ @dry_run_result = result
88
+
89
+ respond_to do |format|
90
+ format.html { render :dry_run }
91
+ format.json { render json: result }
92
+ end
93
+ else
94
+ # Real rerun - execute the agent
95
+ begin
96
+ agent_class.call(**original_params)
97
+ flash[:notice] = "Execution rerun successfully! Check the executions list for the new result."
98
+ rescue StandardError => e
99
+ flash[:alert] = "Rerun failed: #{e.message}"
100
+ end
101
+
102
+ redirect_to executions_path
103
+ end
104
+ end
105
+
106
+ # Exports filtered executions as CSV
107
+ #
108
+ # Streams CSV data with redacted error messages to protect
109
+ # sensitive information. Respects all current filter parameters.
110
+ #
111
+ # @return [void]
112
+ def export
113
+ filename = "executions-#{Date.current.iso8601}.csv"
114
+
115
+ headers["Content-Type"] = "text/csv"
116
+ headers["Content-Disposition"] = "attachment; filename=\"#{filename}\""
117
+
118
+ response.status = 200
119
+
120
+ self.response_body = Enumerator.new do |yielder|
121
+ yielder << CSV.generate_line(CSV_COLUMNS)
122
+
123
+ filtered_executions.find_each(batch_size: 1000) do |execution|
124
+ yielder << generate_csv_row(execution)
125
+ end
126
+ end
127
+ end
128
+
40
129
  private
41
130
 
42
- def load_paginated_executions
43
- page = (params[:page] || 1).to_i
44
- per_page = 25
45
- offset = (page - 1) * per_page
131
+ # Generates a CSV row for a single execution with redacted values
132
+ #
133
+ # @param execution [Execution] The execution record
134
+ # @return [String] CSV row string
135
+ def generate_csv_row(execution)
136
+ redacted_error_message = if execution.error_message.present?
137
+ Redactor.redact_string(execution.error_message)
138
+ end
46
139
 
47
- base_scope = filtered_executions.order(created_at: :desc)
48
- total_count = base_scope.count
49
- @executions = base_scope.limit(per_page).offset(offset)
140
+ CSV.generate_line([
141
+ execution.id,
142
+ execution.agent_type,
143
+ execution.agent_version,
144
+ execution.status,
145
+ execution.model_id,
146
+ execution.total_tokens,
147
+ execution.total_cost&.to_f,
148
+ execution.duration_ms,
149
+ execution.created_at.iso8601,
150
+ execution.error_class,
151
+ redacted_error_message
152
+ ])
153
+ end
50
154
 
51
- @pagination = {
52
- current_page: page,
53
- per_page: per_page,
54
- total_count: total_count,
55
- total_pages: (total_count.to_f / per_page).ceil
56
- }
155
+ # Loads available options for filter dropdowns
156
+ #
157
+ # Populates @agent_types with all agent types that have executions,
158
+ # @model_ids with all distinct models used, and @statuses with all
159
+ # possible status values.
160
+ #
161
+ # @return [void]
162
+ def load_filter_options
163
+ @agent_types = available_agent_types
164
+ @model_ids = available_model_ids
165
+ @statuses = Execution.statuses.keys
57
166
  end
58
167
 
168
+ # Returns distinct agent types from execution history
169
+ #
170
+ # Memoized to avoid duplicate queries within a request.
171
+ #
172
+ # @return [Array<String>] Agent type names
173
+ def available_agent_types
174
+ @available_agent_types ||= Execution.distinct.pluck(:agent_type)
175
+ end
176
+
177
+ # Returns distinct model IDs from execution history
178
+ #
179
+ # Memoized to avoid duplicate queries within a request.
180
+ #
181
+ # @return [Array<String>] Model IDs
182
+ def available_model_ids
183
+ @available_model_ids ||= Execution.where.not(model_id: nil).distinct.pluck(:model_id).sort
184
+ end
185
+
186
+ # Loads paginated executions and associated statistics
187
+ #
188
+ # Sets @executions, @pagination, and @filter_stats instance variables
189
+ # for use in views.
190
+ #
191
+ # @return [void]
192
+ def load_executions_with_stats
193
+ result = paginate(filtered_executions)
194
+ @executions = result[:records]
195
+ @pagination = result[:pagination]
196
+ load_filter_stats
197
+ end
198
+
199
+ # Calculates aggregate statistics for the current filter
200
+ #
201
+ # @return [void]
59
202
  def load_filter_stats
60
203
  scope = filtered_executions
61
204
  @filter_stats = {
@@ -65,26 +208,41 @@ module RubyLLM
65
208
  }
66
209
  end
67
210
 
211
+ # Builds a filtered execution scope based on request params
212
+ #
213
+ # Applies filters in order: search, agent type, status, then time range.
214
+ # Each filter is optional and validated before application.
215
+ #
216
+ # @return [ActiveRecord::Relation] Filtered execution scope
68
217
  def filtered_executions
69
218
  scope = Execution.all
70
219
 
71
- # Support multiple agent types (comma-separated or array)
72
- if params[:agent_types].present?
73
- agent_types = params[:agent_types].is_a?(Array) ? params[:agent_types] : params[:agent_types].split(",")
74
- scope = scope.where(agent_type: agent_types) if agent_types.any?(&:present?)
220
+ # Apply search filter
221
+ scope = scope.search(params[:q]) if params[:q].present?
222
+
223
+ # Apply agent type filter
224
+ agent_types = parse_array_param(:agent_types)
225
+ if agent_types.any?
226
+ scope = scope.where(agent_type: agent_types)
75
227
  elsif params[:agent_type].present?
76
228
  scope = scope.by_agent(params[:agent_type])
77
229
  end
78
230
 
79
- # Support multiple statuses (comma-separated or array)
80
- if params[:statuses].present?
81
- statuses = params[:statuses].is_a?(Array) ? params[:statuses] : params[:statuses].split(",")
82
- scope = scope.where(status: statuses) if statuses.any?(&:present?)
231
+ # Apply status filter with validation
232
+ statuses = parse_array_param(:statuses)
233
+ if statuses.any?
234
+ scope = apply_status_filter(scope, statuses)
83
235
  elsif params[:status].present?
84
- scope = scope.where(status: params[:status])
236
+ scope = apply_status_filter(scope, [params[:status]])
85
237
  end
86
238
 
87
- scope = scope.where("created_at >= ?", params[:days].to_i.days.ago) if params[:days].present?
239
+ # Apply time filter with validation
240
+ days = parse_days_param
241
+ scope = apply_time_filter(scope, days)
242
+
243
+ # Apply model filter
244
+ model_ids = parse_array_param(:model_ids)
245
+ scope = scope.where(model_id: model_ids) if model_ids.any?
88
246
 
89
247
  scope
90
248
  end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module Agents
5
+ # Controller for displaying global configuration settings
6
+ #
7
+ # Shows all configuration options from RubyLLM::Agents.configuration
8
+ # in a read-only dashboard view.
9
+ #
10
+ # @api private
11
+ class SettingsController < ApplicationController
12
+ # Displays the global configuration settings
13
+ #
14
+ # @return [void]
15
+ def show
16
+ @config = RubyLLM::Agents.configuration
17
+ end
18
+ end
19
+ end
20
+ end