ruby_llm-agents 3.11.0 → 3.13.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 (48) hide show
  1. checksums.yaml +4 -4
  2. data/app/controllers/ruby_llm/agents/agents_controller.rb +74 -0
  3. data/app/controllers/ruby_llm/agents/analytics_controller.rb +304 -0
  4. data/app/controllers/ruby_llm/agents/executions_controller.rb +5 -0
  5. data/app/controllers/ruby_llm/agents/tenants_controller.rb +74 -2
  6. data/app/models/ruby_llm/agents/agent_override.rb +47 -0
  7. data/app/models/ruby_llm/agents/execution/analytics.rb +37 -16
  8. data/app/models/ruby_llm/agents/execution.rb +51 -1
  9. data/app/services/ruby_llm/agents/agent_registry.rb +8 -1
  10. data/app/views/layouts/ruby_llm/agents/application.html.erb +4 -2
  11. data/app/views/ruby_llm/agents/agents/_config_agent.html.erb +93 -4
  12. data/app/views/ruby_llm/agents/agents/show.html.erb +17 -2
  13. data/app/views/ruby_llm/agents/analytics/index.html.erb +398 -0
  14. data/app/views/ruby_llm/agents/executions/_audio_player.html.erb +1 -1
  15. data/app/views/ruby_llm/agents/executions/_filters.html.erb +12 -8
  16. data/app/views/ruby_llm/agents/executions/show.html.erb +26 -12
  17. data/app/views/ruby_llm/agents/shared/_filter_dropdown.html.erb +46 -7
  18. data/app/views/ruby_llm/agents/shared/_tenant_filter.html.erb +2 -2
  19. data/app/views/ruby_llm/agents/system_config/show.html.erb +6 -2
  20. data/app/views/ruby_llm/agents/tenants/index.html.erb +3 -2
  21. data/app/views/ruby_llm/agents/tenants/show.html.erb +225 -0
  22. data/config/routes.rb +12 -4
  23. data/lib/generators/ruby_llm_agents/templates/create_overrides_migration.rb.tt +28 -0
  24. data/lib/generators/ruby_llm_agents/templates/initializer.rb.tt +27 -1
  25. data/lib/generators/ruby_llm_agents/templates/skills/AGENTS.md.tt +1 -1
  26. data/lib/generators/ruby_llm_agents/templates/skills/TOOLS.md.tt +1 -1
  27. data/lib/generators/ruby_llm_agents/upgrade_generator.rb +14 -0
  28. data/lib/ruby_llm/agents/base_agent.rb +90 -133
  29. data/lib/ruby_llm/agents/core/base.rb +9 -0
  30. data/lib/ruby_llm/agents/core/configuration.rb +93 -7
  31. data/lib/ruby_llm/agents/core/version.rb +1 -1
  32. data/lib/ruby_llm/agents/dsl/base.rb +131 -4
  33. data/lib/ruby_llm/agents/dsl/knowledge.rb +157 -0
  34. data/lib/ruby_llm/agents/dsl.rb +1 -1
  35. data/lib/ruby_llm/agents/image/concerns/image_operation_execution.rb +9 -5
  36. data/lib/ruby_llm/agents/infrastructure/retention_job.rb +118 -0
  37. data/lib/ruby_llm/agents/pipeline/middleware/budget.rb +32 -20
  38. data/lib/ruby_llm/agents/pipeline/middleware/instrumentation.rb +22 -1
  39. data/lib/ruby_llm/agents/pipeline/middleware/reliability.rb +1 -1
  40. data/lib/ruby_llm/agents/rails/engine.rb +20 -4
  41. data/lib/ruby_llm/agents/routing.rb +28 -5
  42. data/lib/ruby_llm/agents/stream_event.rb +2 -10
  43. data/lib/ruby_llm/agents/tool.rb +1 -1
  44. data/lib/ruby_llm/agents.rb +1 -3
  45. data/lib/tasks/ruby_llm_agents.rake +7 -0
  46. metadata +9 -5
  47. data/lib/ruby_llm/agents/agent_tool.rb +0 -143
  48. data/lib/ruby_llm/agents/dsl/agents.rb +0 -141
@@ -51,8 +51,12 @@
51
51
  <span class="badge badge-sm <%= @config.async_logging ? 'badge-success' : 'badge-timeout' %>"><%= @config.async_logging ? 'on' : 'off' %></span>
52
52
  </div>
53
53
  <div class="flex items-center gap-3 py-0.5">
54
- <span class="w-36 flex-shrink-0 text-gray-500 dark:text-gray-400">retention</span>
55
- <span class="text-gray-900 dark:text-gray-200"><%= @config.retention_period.inspect %></span>
54
+ <span class="w-36 flex-shrink-0 text-gray-500 dark:text-gray-400">soft purge after</span>
55
+ <span class="text-gray-900 dark:text-gray-200"><%= @config.soft_purge_after ? @config.soft_purge_after.inspect : "disabled" %></span>
56
+ </div>
57
+ <div class="flex items-center gap-3 py-0.5">
58
+ <span class="w-36 flex-shrink-0 text-gray-500 dark:text-gray-400">hard purge after</span>
59
+ <span class="text-gray-900 dark:text-gray-200"><%= @config.hard_purge_after ? @config.hard_purge_after.inspect : "disabled" %></span>
56
60
  </div>
57
61
  <div class="flex items-center gap-3 py-0.5">
58
62
  <span class="w-36 flex-shrink-0 text-gray-500 dark:text-gray-400">job retries</span>
@@ -65,7 +65,8 @@
65
65
  when "soft" then "badge-timeout"
66
66
  else ""
67
67
  end
68
- last_execution = tenant.executions.order(created_at: :desc).pick(:created_at)
68
+ last_execution = @tenant_last_executions[tenant.tenant_id]
69
+ tenant_cost = @tenant_costs[tenant.tenant_id] || 0
69
70
  %>
70
71
  <div class="group flex items-center gap-3 py-1.5 px-2 -mx-2 rounded hover:bg-gray-100 dark:hover:bg-gray-800/50 cursor-pointer"
71
72
  onclick="window.location='<%= tenant_path(tenant) %>'">
@@ -96,7 +97,7 @@
96
97
  <span class="text-gray-300 dark:text-gray-700">none</span>
97
98
  <% end %>
98
99
  </span>
99
- <span class="w-16 flex-shrink-0 text-right text-gray-500 dark:text-gray-400 hidden md:inline">$<%= number_with_precision(tenant.cost, precision: 2) %></span>
100
+ <span class="w-16 flex-shrink-0 text-right text-gray-500 dark:text-gray-400 hidden md:inline">$<%= number_with_precision(tenant_cost, precision: 2) %></span>
100
101
  <span class="w-24 flex-shrink-0 text-gray-400 dark:text-gray-600 text-right whitespace-nowrap">
101
102
  <% if last_execution %>
102
103
  <%= time_ago_in_words(last_execution) %>
@@ -34,6 +34,11 @@
34
34
  <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
35
35
  </svg>
36
36
  <% end %>
37
+ <%= button_to refresh_counters_tenant_path(@tenant), method: :post, class: "text-gray-400 dark:text-gray-500 hover:text-gray-600 dark:hover:text-gray-300 transition-colors", title: "Refresh budget counters", form: { style: "display:inline" } do %>
38
+ <svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
39
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
40
+ </svg>
41
+ <% end %>
37
42
  <%= render "ruby_llm/agents/shared/doc_link" %>
38
43
  </div>
39
44
  <div class="font-mono text-xs text-gray-400 dark:text-gray-500 flex items-center gap-1.5 flex-wrap">
@@ -70,6 +75,153 @@
70
75
  </div>
71
76
  <div class="border-t border-gray-200 dark:border-gray-800 mb-2"></div>
72
77
 
78
+ <!-- ── period comparison ──────────────── -->
79
+ <% if @period_comparison %>
80
+ <%
81
+ pc = @period_comparison
82
+ tm = pc[:this_month]
83
+ lm = pc[:last_month]
84
+ %>
85
+ <div class="flex items-center gap-3 mt-6 mb-3">
86
+ <span class="text-[10px] font-medium text-gray-400 dark:text-gray-600 uppercase tracking-widest font-mono">this month vs last month</span>
87
+ <div class="flex-1 border-t border-gray-200 dark:border-gray-800"></div>
88
+ </div>
89
+
90
+ <div class="grid grid-cols-2 sm:grid-cols-4 gap-4 font-mono text-xs">
91
+ <% [
92
+ ["cost", tm[:cost], lm[:cost], pc[:cost_change], "$"],
93
+ ["avg cost/run", pc[:avg_cost_this], pc[:avg_cost_last], pc[:avg_cost_change], "$"],
94
+ ["runs", tm[:executions], lm[:executions], pc[:executions_change], ""],
95
+ ["tokens", tm[:tokens], lm[:tokens], pc[:tokens_change], ""]
96
+ ].each do |label, current, previous, change, prefix| %>
97
+ <div class="space-y-0.5">
98
+ <div class="text-[10px] text-gray-400 dark:text-gray-600 uppercase tracking-wider"><%= label %></div>
99
+ <div class="text-gray-900 dark:text-gray-200">
100
+ <% if prefix == "$" %>
101
+ $<%= number_with_precision(current, precision: 4) %>
102
+ <% else %>
103
+ <%= number_with_delimiter(current) %>
104
+ <% end %>
105
+ </div>
106
+ <div class="flex items-center gap-1">
107
+ <% if change != 0 %>
108
+ <span class="<%= change > 0 ? 'text-red-500' : 'text-green-500' %>">
109
+ <%= change > 0 ? "+" : "" %><%= change %>%
110
+ </span>
111
+ <% else %>
112
+ <span class="text-gray-400 dark:text-gray-600">—</span>
113
+ <% end %>
114
+ <span class="text-gray-400 dark:text-gray-600">
115
+ vs
116
+ <% if prefix == "$" %>
117
+ $<%= number_with_precision(previous, precision: 4) %>
118
+ <% else %>
119
+ <%= number_with_delimiter(previous) %>
120
+ <% end %>
121
+ </span>
122
+ </div>
123
+ </div>
124
+ <% end %>
125
+ </div>
126
+
127
+ <% if @error_count > 0 %>
128
+ <div class="mt-3 font-mono text-xs text-gray-500 dark:text-gray-400">
129
+ <span class="text-red-500">$<%= number_with_precision(@error_cost, precision: 4) %></span>
130
+ wasted on <span class="text-red-500"><%= number_with_delimiter(@error_count) %></span> failed executions
131
+ <% error_pct = total_cost.to_f > 0 ? (@error_cost.to_f / total_cost * 100).round(1) : 0 %>
132
+ <% if error_pct > 0 %>
133
+ (<%= error_pct %>% of total cost)
134
+ <% end %>
135
+ </div>
136
+ <% end %>
137
+ <% end %>
138
+
139
+ <!-- ── 30d cost trend ──────────────── -->
140
+ <% if @daily_trend.present? %>
141
+ <div class="flex items-center gap-3 mt-6 mb-2">
142
+ <span class="text-[10px] font-medium text-gray-400 dark:text-gray-600 uppercase tracking-widest font-mono">last 30 days</span>
143
+ <div class="flex-1 border-t border-gray-200 dark:border-gray-800"></div>
144
+ </div>
145
+ <div id="tenant-trend-chart" style="width: 100%; height: 160px;"></div>
146
+ <script>
147
+ (function() {
148
+ const trendData = <%= raw @daily_trend.map { |date, stats|
149
+ { date: date.to_s, cost: (stats[:cost] || 0).to_f.round(6),
150
+ tokens: (stats[:tokens] || 0).to_i, count: (stats[:count] || 0).to_i }
151
+ }.to_json %>;
152
+
153
+ const costData = trendData.map(d => [new Date(d.date).getTime(), d.cost]);
154
+ const countData = trendData.map(d => [new Date(d.date).getTime(), d.count]);
155
+
156
+ function renderChart() {
157
+ window.__initHighchartsDefaults && window.__initHighchartsDefaults();
158
+ Highcharts.chart('tenant-trend-chart', {
159
+ chart: { backgroundColor: 'transparent', spacing: [5, 0, 0, 0] },
160
+ title: { text: null },
161
+ xAxis: {
162
+ type: 'datetime',
163
+ labels: { style: { color: chartColor('#6B7280', '#928374'), fontSize: '9px', fontFamily: 'ui-monospace, monospace' }, format: '{value:%b %d}' },
164
+ lineColor: 'transparent', tickLength: 0, gridLineWidth: 0
165
+ },
166
+ yAxis: [
167
+ {
168
+ title: { text: null }, min: 0,
169
+ labels: { style: { color: chartColor('#6B7280', '#928374'), fontSize: '9px', fontFamily: 'ui-monospace, monospace' }, format: '${value}' },
170
+ gridLineColor: chartColorAlpha('rgba(107, 114, 128, 0.08)', 146, 131, 116, 0.08)
171
+ },
172
+ {
173
+ title: { text: null }, min: 0, opposite: true, allowDecimals: false,
174
+ labels: { style: { color: chartColor('#6B7280', '#928374'), fontSize: '9px', fontFamily: 'ui-monospace, monospace' } },
175
+ gridLineWidth: 0
176
+ }
177
+ ],
178
+ legend: { enabled: false },
179
+ credits: { enabled: false },
180
+ tooltip: {
181
+ backgroundColor: chartColorAlpha('rgba(0, 0, 0, 0.85)', 40, 40, 40, 0.95),
182
+ borderColor: 'transparent', borderRadius: 3,
183
+ style: { color: chartColor('#E5E7EB', '#ebdbb2'), fontSize: '10px', fontFamily: 'ui-monospace, monospace' },
184
+ shared: true,
185
+ formatter: function() {
186
+ let html = '<span style="color:' + chartColor('#9CA3AF', '#bdae93') + '">' + Highcharts.dateFormat('%b %d', this.x) + '</span>';
187
+ let d = trendData.find(d => new Date(d.date).getTime() === this.x);
188
+ if (d) {
189
+ html += '<br/>cost: <b>$' + d.cost.toFixed(4) + '</b>';
190
+ html += '<br/>runs: <b>' + d.count + '</b>';
191
+ html += '<br/>tokens: <b>' + d.tokens.toLocaleString() + '</b>';
192
+ }
193
+ return html;
194
+ }
195
+ },
196
+ series: [
197
+ {
198
+ name: 'cost', type: 'areaspline', yAxis: 0, data: costData,
199
+ color: chartColor('#8B5CF6', '#d3869b'),
200
+ lineWidth: 1.5, marker: { enabled: false, states: { hover: { enabled: true, radius: 2 } } },
201
+ fillColor: { linearGradient: { x1: 0, y1: 0, x2: 0, y2: 1 },
202
+ stops: [[0, chartColorAlpha('rgba(139, 92, 246, 0.12)', 211, 134, 155, 0.12)], [1, chartColorAlpha('rgba(139, 92, 246, 0)', 211, 134, 155, 0)]] }
203
+ },
204
+ {
205
+ name: 'runs', type: 'column', yAxis: 1, data: countData,
206
+ color: chartColorAlpha('rgba(107, 114, 128, 0.15)', 146, 131, 116, 0.15),
207
+ borderWidth: 0, pointPadding: 0.1, groupPadding: 0
208
+ }
209
+ ]
210
+ });
211
+ }
212
+
213
+ function waitForHighcharts(attempts) {
214
+ if (typeof Highcharts !== 'undefined') { renderChart(); }
215
+ else if (attempts > 0) { setTimeout(function() { waitForHighcharts(attempts - 1); }, 100); }
216
+ }
217
+
218
+ document.readyState === 'loading'
219
+ ? document.addEventListener('DOMContentLoaded', function() { waitForHighcharts(50); })
220
+ : waitForHighcharts(50);
221
+ })();
222
+ </script>
223
+ <% end %>
224
+
73
225
  <% if has_any_limit %>
74
226
  <!-- ── budget ──────────────────────── -->
75
227
  <div class="flex items-center gap-3 mt-6 mb-3">
@@ -166,6 +318,79 @@
166
318
  </div>
167
319
  <% end %>
168
320
 
321
+ <% if @usage_by_agent.present? %>
322
+ <!-- ── usage by agent ──────────────────── -->
323
+ <div class="flex items-center gap-3 mt-6 mb-3">
324
+ <span class="text-[10px] font-medium text-gray-400 dark:text-gray-600 uppercase tracking-widest font-mono">usage by agent</span>
325
+ <div class="flex-1 border-t border-gray-200 dark:border-gray-800"></div>
326
+ </div>
327
+
328
+ <!-- Column headers -->
329
+ <div class="flex items-center gap-3 px-2 -mx-2 font-mono text-[10px] text-gray-400 dark:text-gray-600 uppercase tracking-wider mb-1">
330
+ <span class="flex-[2] min-w-0">agent</span>
331
+ <span class="w-16 flex-shrink-0 text-right">runs</span>
332
+ <span class="w-24 flex-shrink-0 text-right">cost</span>
333
+ <span class="w-20 flex-shrink-0 text-right">avg cost</span>
334
+ <span class="w-24 flex-shrink-0 text-right">tokens</span>
335
+ <span class="w-20 flex-shrink-0 text-right">avg tokens</span>
336
+ </div>
337
+
338
+ <div class="font-mono text-xs space-y-px">
339
+ <% @usage_by_agent.sort_by { |_, v| -v[:cost] }.each do |agent_type, stats| %>
340
+ <%
341
+ avg_cost = stats[:count] > 0 ? (stats[:cost].to_f / stats[:count]) : 0
342
+ avg_tokens = stats[:count] > 0 ? (stats[:tokens].to_f / stats[:count]).round : 0
343
+ %>
344
+ <div class="flex items-center gap-3 py-1 px-2 -mx-2 rounded hover:bg-gray-100 dark:hover:bg-gray-800/50">
345
+ <span class="flex-[2] min-w-0 truncate">
346
+ <%= link_to agent_type.to_s.demodulize, ruby_llm_agents.agent_path(agent_type),
347
+ class: "text-gray-900 dark:text-gray-200 hover:text-gray-600 dark:hover:text-gray-400" %>
348
+ </span>
349
+ <span class="w-16 flex-shrink-0 text-right text-gray-500 dark:text-gray-400"><%= number_with_delimiter(stats[:count]) %></span>
350
+ <span class="w-24 flex-shrink-0 text-right text-gray-800 dark:text-gray-200">$<%= number_with_precision(stats[:cost], precision: 4) %></span>
351
+ <span class="w-20 flex-shrink-0 text-right text-gray-500 dark:text-gray-400">$<%= number_with_precision(avg_cost, precision: 4) %></span>
352
+ <span class="w-24 flex-shrink-0 text-right text-gray-600 dark:text-gray-400"><%= number_with_delimiter(stats[:tokens]) %></span>
353
+ <span class="w-20 flex-shrink-0 text-right text-gray-500 dark:text-gray-400"><%= number_with_delimiter(avg_tokens) %></span>
354
+ </div>
355
+ <% end %>
356
+ </div>
357
+ <% end %>
358
+
359
+ <% if @usage_by_model.present? %>
360
+ <!-- ── usage by model ──────────────────── -->
361
+ <div class="flex items-center gap-3 mt-6 mb-3">
362
+ <span class="text-[10px] font-medium text-gray-400 dark:text-gray-600 uppercase tracking-widest font-mono">usage by model</span>
363
+ <div class="flex-1 border-t border-gray-200 dark:border-gray-800"></div>
364
+ </div>
365
+
366
+ <!-- Column headers -->
367
+ <div class="flex items-center gap-3 px-2 -mx-2 font-mono text-[10px] text-gray-400 dark:text-gray-600 uppercase tracking-wider mb-1">
368
+ <span class="flex-[2] min-w-0">model</span>
369
+ <span class="w-16 flex-shrink-0 text-right">runs</span>
370
+ <span class="w-24 flex-shrink-0 text-right">cost</span>
371
+ <span class="w-20 flex-shrink-0 text-right">avg cost</span>
372
+ <span class="w-24 flex-shrink-0 text-right">tokens</span>
373
+ <span class="w-16 flex-shrink-0 text-right">% of cost</span>
374
+ </div>
375
+
376
+ <div class="font-mono text-xs space-y-px">
377
+ <% @usage_by_model.sort_by { |_, v| -v[:cost] }.each do |model_id, stats| %>
378
+ <%
379
+ avg_cost = stats[:count] > 0 ? (stats[:cost].to_f / stats[:count]) : 0
380
+ cost_pct = total_cost.to_f > 0 ? (stats[:cost].to_f / total_cost * 100).round(1) : 0
381
+ %>
382
+ <div class="flex items-center gap-3 py-1 px-2 -mx-2 rounded hover:bg-gray-100 dark:hover:bg-gray-800/50">
383
+ <span class="flex-[2] min-w-0 truncate text-gray-900 dark:text-gray-200"><%= model_id %></span>
384
+ <span class="w-16 flex-shrink-0 text-right text-gray-500 dark:text-gray-400"><%= number_with_delimiter(stats[:count]) %></span>
385
+ <span class="w-24 flex-shrink-0 text-right text-gray-800 dark:text-gray-200">$<%= number_with_precision(stats[:cost], precision: 4) %></span>
386
+ <span class="w-20 flex-shrink-0 text-right text-gray-500 dark:text-gray-400">$<%= number_with_precision(avg_cost, precision: 4) %></span>
387
+ <span class="w-24 flex-shrink-0 text-right text-gray-600 dark:text-gray-400"><%= number_with_delimiter(stats[:tokens]) %></span>
388
+ <span class="w-16 flex-shrink-0 text-right text-gray-400 dark:text-gray-600"><%= cost_pct %>%</span>
389
+ </div>
390
+ <% end %>
391
+ </div>
392
+ <% end %>
393
+
169
394
  <!-- ── recent executions ──────────────────── -->
170
395
  <div class="flex items-center gap-3 mt-6 mb-3">
171
396
  <span class="text-[10px] font-medium text-gray-400 dark:text-gray-600 uppercase tracking-widest font-mono">recent executions</span>
data/config/routes.rb CHANGED
@@ -4,7 +4,11 @@ RubyLLM::Agents::Engine.routes.draw do
4
4
  root to: "dashboard#index"
5
5
  get "chart_data", to: "dashboard#chart_data"
6
6
 
7
- resources :agents, only: [:index, :show]
7
+ resources :agents, only: [:index, :show, :update] do
8
+ member do
9
+ delete :reset_overrides
10
+ end
11
+ end
8
12
 
9
13
  resources :executions, only: [:index, :show] do
10
14
  collection do
@@ -15,9 +19,13 @@ RubyLLM::Agents::Engine.routes.draw do
15
19
 
16
20
  resources :requests, only: [:index, :show]
17
21
 
18
- resources :tenants, only: [:index, :show, :edit, :update]
22
+ resources :tenants, only: [:index, :show, :edit, :update] do
23
+ member do
24
+ post :refresh_counters
25
+ end
26
+ end
19
27
 
20
- # Redirect old analytics route to dashboard
21
- get "analytics", to: redirect("/")
28
+ get "analytics", to: "analytics#index", as: :analytics
29
+ get "analytics/chart_data", to: "analytics#chart_data", as: :analytics_chart_data
22
30
  resource :system_config, only: [:show], controller: "system_config"
23
31
  end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Migration to create the agent overrides table for dashboard-managed settings
4
+ #
5
+ # This table stores per-agent setting overrides that are managed through
6
+ # the dashboard UI. Only fields declared as `overridable: true` in the
7
+ # agent DSL can be overridden.
8
+ #
9
+ # Run with: rails db:migrate
10
+ class CreateRubyLLMAgentsOverrides < ActiveRecord::Migration<%= migration_version %>
11
+ def change
12
+ create_table :ruby_llm_agents_overrides do |t|
13
+ # The agent class name (e.g., "SupportAgent")
14
+ t.string :agent_type, null: false
15
+
16
+ # JSON hash of overridden settings
17
+ # Format: { "model" => "gpt-4o-mini", "temperature" => 0.5 }
18
+ t.json :settings, null: false, default: {}
19
+
20
+ # Who last changed the override (optional audit trail)
21
+ t.string :updated_by
22
+
23
+ t.timestamps
24
+ end
25
+
26
+ add_index :ruby_llm_agents_overrides, :agent_type, unique: true
27
+ end
28
+ end
@@ -67,7 +67,33 @@ RubyLLM::Agents.configure do |config|
67
67
  # Number of retry attempts for the async logging job on failure
68
68
  # config.job_retry_attempts = 3
69
69
 
70
- # Retention period for execution records (used by cleanup tasks)
70
+ # ============================================
71
+ # Data Retention
72
+ # ============================================
73
+ #
74
+ # Two-tier purge of execution records. Run the retention job on a schedule
75
+ # (e.g. daily via cron, sidekiq-cron, or the `whenever` gem):
76
+ #
77
+ # RubyLLM::Agents::RetentionJob.perform_later
78
+ # # or: rake ruby_llm_agents:purge
79
+ #
80
+ # Soft purge: deletes prompts, responses, tool calls, attempts, and other
81
+ # large payloads in execution_details and tool_executions. The executions
82
+ # row is preserved so cost, token, and latency analytics stay intact. A
83
+ # truncated copy of error_message is kept in executions.metadata.
84
+ #
85
+ # Hard purge: deletes the executions row entirely (cascades remove any
86
+ # remaining dependents). Use a longer window — this removes the execution
87
+ # from analytics.
88
+ #
89
+ # Set either to nil to disable that tier. soft_purge_after must be less
90
+ # than hard_purge_after when both are set.
91
+ #
92
+ # config.soft_purge_after = 30.days
93
+ # config.hard_purge_after = 365.days
94
+
95
+ # Deprecated: retention_period is an alias for hard_purge_after. Prefer the
96
+ # two-tier settings above.
71
97
  # config.retention_period = 30.days
72
98
 
73
99
  # ============================================
@@ -110,7 +110,7 @@ streaming true # Enable streaming by default
110
110
  ### Tools
111
111
 
112
112
  ```ruby
113
- tools [SearchTool, CalculatorTool] # Make tools available to agent
113
+ tools SearchTool, CalculatorTool # Make tools available to agent
114
114
  ```
115
115
 
116
116
  ### Extended Thinking
@@ -72,7 +72,7 @@ module Agents
72
72
  class ResearchAgent < ApplicationAgent
73
73
  model "gpt-4o"
74
74
 
75
- tools [SearchTool, CalculatorTool, WebFetchTool]
75
+ tools SearchTool, CalculatorTool, WebFetchTool
76
76
 
77
77
  param :question, required: true
78
78
 
@@ -117,6 +117,20 @@ module RubyLlmAgents
117
117
  )
118
118
  end
119
119
 
120
+ # Create overrides table for dashboard-managed agent settings
121
+ def create_overrides_migration
122
+ if table_exists?(:ruby_llm_agents_overrides)
123
+ say_status :skip, "ruby_llm_agents_overrides table already exists", :yellow
124
+ return
125
+ end
126
+
127
+ say_status :upgrade, "Creating agent overrides table", :blue
128
+ migration_template(
129
+ "create_overrides_migration.rb.tt",
130
+ File.join(db_migrate_path, "create_ruby_llm_agents_overrides.rb")
131
+ )
132
+ end
133
+
120
134
  def suggest_config_consolidation
121
135
  ruby_llm_initializer = File.join(destination_root, "config/initializers/ruby_llm.rb")
122
136
  agents_initializer = File.join(destination_root, "config/initializers/ruby_llm_agents.rb")