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.
- checksums.yaml +4 -4
- data/app/controllers/ruby_llm/agents/agents_controller.rb +74 -0
- data/app/controllers/ruby_llm/agents/analytics_controller.rb +304 -0
- data/app/controllers/ruby_llm/agents/executions_controller.rb +5 -0
- data/app/controllers/ruby_llm/agents/tenants_controller.rb +74 -2
- data/app/models/ruby_llm/agents/agent_override.rb +47 -0
- data/app/models/ruby_llm/agents/execution/analytics.rb +37 -16
- data/app/models/ruby_llm/agents/execution.rb +51 -1
- data/app/services/ruby_llm/agents/agent_registry.rb +8 -1
- data/app/views/layouts/ruby_llm/agents/application.html.erb +4 -2
- data/app/views/ruby_llm/agents/agents/_config_agent.html.erb +93 -4
- data/app/views/ruby_llm/agents/agents/show.html.erb +17 -2
- data/app/views/ruby_llm/agents/analytics/index.html.erb +398 -0
- data/app/views/ruby_llm/agents/executions/_audio_player.html.erb +1 -1
- data/app/views/ruby_llm/agents/executions/_filters.html.erb +12 -8
- data/app/views/ruby_llm/agents/executions/show.html.erb +26 -12
- data/app/views/ruby_llm/agents/shared/_filter_dropdown.html.erb +46 -7
- data/app/views/ruby_llm/agents/shared/_tenant_filter.html.erb +2 -2
- data/app/views/ruby_llm/agents/system_config/show.html.erb +6 -2
- data/app/views/ruby_llm/agents/tenants/index.html.erb +3 -2
- data/app/views/ruby_llm/agents/tenants/show.html.erb +225 -0
- data/config/routes.rb +12 -4
- data/lib/generators/ruby_llm_agents/templates/create_overrides_migration.rb.tt +28 -0
- data/lib/generators/ruby_llm_agents/templates/initializer.rb.tt +27 -1
- data/lib/generators/ruby_llm_agents/templates/skills/AGENTS.md.tt +1 -1
- data/lib/generators/ruby_llm_agents/templates/skills/TOOLS.md.tt +1 -1
- data/lib/generators/ruby_llm_agents/upgrade_generator.rb +14 -0
- data/lib/ruby_llm/agents/base_agent.rb +90 -133
- data/lib/ruby_llm/agents/core/base.rb +9 -0
- data/lib/ruby_llm/agents/core/configuration.rb +93 -7
- data/lib/ruby_llm/agents/core/version.rb +1 -1
- data/lib/ruby_llm/agents/dsl/base.rb +131 -4
- data/lib/ruby_llm/agents/dsl/knowledge.rb +157 -0
- data/lib/ruby_llm/agents/dsl.rb +1 -1
- data/lib/ruby_llm/agents/image/concerns/image_operation_execution.rb +9 -5
- data/lib/ruby_llm/agents/infrastructure/retention_job.rb +118 -0
- data/lib/ruby_llm/agents/pipeline/middleware/budget.rb +32 -20
- data/lib/ruby_llm/agents/pipeline/middleware/instrumentation.rb +22 -1
- data/lib/ruby_llm/agents/pipeline/middleware/reliability.rb +1 -1
- data/lib/ruby_llm/agents/rails/engine.rb +20 -4
- data/lib/ruby_llm/agents/routing.rb +28 -5
- data/lib/ruby_llm/agents/stream_event.rb +2 -10
- data/lib/ruby_llm/agents/tool.rb +1 -1
- data/lib/ruby_llm/agents.rb +1 -3
- data/lib/tasks/ruby_llm_agents.rake +7 -0
- metadata +9 -5
- data/lib/ruby_llm/agents/agent_tool.rb +0 -143
- 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">
|
|
55
|
-
<span class="text-gray-900 dark:text-gray-200"><%= @config.
|
|
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.
|
|
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(
|
|
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
|
-
|
|
21
|
-
get "analytics", to:
|
|
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
|
-
#
|
|
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
|
|
113
|
+
tools SearchTool, CalculatorTool # Make tools available to agent
|
|
114
114
|
```
|
|
115
115
|
|
|
116
116
|
### Extended Thinking
|
|
@@ -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")
|