ruby_llm-agents 3.10.0 → 3.12.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/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/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 +89 -4
- data/app/views/ruby_llm/agents/agents/show.html.erb +14 -0
- data/app/views/ruby_llm/agents/analytics/index.html.erb +398 -0
- 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/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 +158 -37
- data/lib/ruby_llm/agents/core/base.rb +9 -0
- data/lib/ruby_llm/agents/core/configuration.rb +5 -1
- 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 -0
- data/lib/ruby_llm/agents/pipeline/context.rb +11 -2
- 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/routing/result.rb +60 -9
- data/lib/ruby_llm/agents/routing.rb +19 -0
- data/lib/ruby_llm/agents/stream_event.rb +58 -0
- data/lib/ruby_llm/agents/tool.rb +1 -1
- data/lib/ruby_llm/agents.rb +2 -2
- metadata +7 -2
- data/lib/ruby_llm/agents/agent_tool.rb +0 -125
|
@@ -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
|
|
@@ -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")
|
|
@@ -47,6 +47,8 @@ module RubyLLM
|
|
|
47
47
|
extend DSL::Reliability
|
|
48
48
|
extend DSL::Caching
|
|
49
49
|
extend DSL::Queryable
|
|
50
|
+
extend DSL::Knowledge
|
|
51
|
+
include DSL::Knowledge::InstanceMethods
|
|
50
52
|
include CacheHelper
|
|
51
53
|
|
|
52
54
|
class << self
|
|
@@ -170,6 +172,7 @@ module RubyLLM
|
|
|
170
172
|
tools: tools.map { |t| t.respond_to?(:name) ? t.name : t.to_s },
|
|
171
173
|
parameters: params.transform_values { |v| v.slice(:type, :required, :default, :desc) },
|
|
172
174
|
thinking: thinking_config,
|
|
175
|
+
cache_prompts: cache_prompts || nil,
|
|
173
176
|
caching: caching_config,
|
|
174
177
|
reliability: reliability_configured? ? reliability_config : nil
|
|
175
178
|
}.compact
|
|
@@ -232,12 +235,18 @@ module RubyLLM
|
|
|
232
235
|
# Enables or returns streaming mode for this agent
|
|
233
236
|
#
|
|
234
237
|
# @param value [Boolean, nil] Whether to enable streaming
|
|
238
|
+
# @param overridable [Boolean, nil] When true, this field can be changed from the dashboard
|
|
235
239
|
# @return [Boolean] The current streaming setting
|
|
236
|
-
def streaming(value = nil)
|
|
240
|
+
def streaming(value = nil, overridable: nil)
|
|
237
241
|
@streaming = value unless value.nil?
|
|
238
|
-
|
|
242
|
+
register_overridable(:streaming) if overridable
|
|
243
|
+
base = if @streaming.nil?
|
|
244
|
+
superclass.respond_to?(:streaming) ? superclass.streaming : default_streaming
|
|
245
|
+
else
|
|
246
|
+
@streaming
|
|
247
|
+
end
|
|
239
248
|
|
|
240
|
-
|
|
249
|
+
apply_override(:streaming, base)
|
|
241
250
|
end
|
|
242
251
|
|
|
243
252
|
# @!endgroup
|
|
@@ -246,10 +255,10 @@ module RubyLLM
|
|
|
246
255
|
|
|
247
256
|
# Sets or returns the tools available to this agent
|
|
248
257
|
#
|
|
249
|
-
# @param tool_classes [Array<Class>] Tool classes to make available
|
|
258
|
+
# @param tool_classes [Class, Array<Class>] Tool classes to make available
|
|
250
259
|
# @return [Array<Class>] The current tools
|
|
251
|
-
def tools(tool_classes
|
|
252
|
-
@tools =
|
|
260
|
+
def tools(*tool_classes)
|
|
261
|
+
@tools = tool_classes.flatten if tool_classes.any?
|
|
253
262
|
@tools || (superclass.respond_to?(:tools) ? superclass.tools : [])
|
|
254
263
|
end
|
|
255
264
|
|
|
@@ -260,10 +269,14 @@ module RubyLLM
|
|
|
260
269
|
# Sets or returns the temperature for LLM responses
|
|
261
270
|
#
|
|
262
271
|
# @param value [Float, nil] Temperature value (0.0-2.0)
|
|
272
|
+
# @param overridable [Boolean, nil] When true, this field can be changed from the dashboard
|
|
263
273
|
# @return [Float] The current temperature setting
|
|
264
|
-
def temperature(value = nil)
|
|
274
|
+
def temperature(value = nil, overridable: nil)
|
|
265
275
|
@temperature = value if value
|
|
266
|
-
|
|
276
|
+
register_overridable(:temperature) if overridable
|
|
277
|
+
base = @temperature || (superclass.respond_to?(:temperature) ? superclass.temperature : default_temperature)
|
|
278
|
+
|
|
279
|
+
apply_override(:temperature, base)
|
|
267
280
|
end
|
|
268
281
|
|
|
269
282
|
# @!endgroup
|
|
@@ -387,14 +400,19 @@ module RubyLLM
|
|
|
387
400
|
# System prompt for LLM instructions
|
|
388
401
|
#
|
|
389
402
|
# If a class-level `system` DSL is defined, it will be used.
|
|
390
|
-
#
|
|
403
|
+
# Knowledge entries declared via `knows` are auto-appended.
|
|
391
404
|
#
|
|
392
405
|
# @return [String, nil] System instructions, or nil for none
|
|
393
406
|
def system_prompt
|
|
394
407
|
system_config = self.class.system_config
|
|
395
|
-
|
|
408
|
+
base = system_config ? resolve_prompt_from_config(system_config) : nil
|
|
396
409
|
|
|
397
|
-
|
|
410
|
+
knowledge = compiled_knowledge
|
|
411
|
+
if knowledge.present?
|
|
412
|
+
base ? "#{base}\n\n#{knowledge}" : knowledge
|
|
413
|
+
else
|
|
414
|
+
base
|
|
415
|
+
end
|
|
398
416
|
end
|
|
399
417
|
|
|
400
418
|
# Assistant prefill to prime the model's response
|
|
@@ -512,6 +530,7 @@ module RubyLLM
|
|
|
512
530
|
tenant: resolve_tenant,
|
|
513
531
|
skip_cache: @options[:skip_cache],
|
|
514
532
|
stream_block: (block if streaming_enabled?),
|
|
533
|
+
stream_events: @options[:stream_events] == true,
|
|
515
534
|
parent_execution_id: @parent_execution_id,
|
|
516
535
|
root_execution_id: @root_execution_id,
|
|
517
536
|
debug: @options[:debug],
|
|
@@ -552,36 +571,33 @@ module RubyLLM
|
|
|
552
571
|
end
|
|
553
572
|
end
|
|
554
573
|
|
|
555
|
-
#
|
|
574
|
+
# Returns the description for a tool class
|
|
556
575
|
#
|
|
557
|
-
#
|
|
558
|
-
#
|
|
559
|
-
|
|
576
|
+
# @param tool [Class] A tool class
|
|
577
|
+
# @return [String] The tool's description
|
|
578
|
+
def tool_description_for(tool)
|
|
579
|
+
if tool.respond_to?(:description) && tool.description
|
|
580
|
+
tool.description
|
|
581
|
+
elsif tool.is_a?(Class) && tool < RubyLLM::Tool
|
|
582
|
+
tool.new.respond_to?(:description) ? tool.new.description : tool.name.to_s
|
|
583
|
+
else
|
|
584
|
+
tool.name.to_s
|
|
585
|
+
end
|
|
586
|
+
end
|
|
587
|
+
|
|
588
|
+
# Resolves tools for this execution
|
|
560
589
|
#
|
|
561
590
|
# @return [Array<Class>] Tool classes to use
|
|
562
591
|
# @raise [ArgumentError] If duplicate tool names are detected
|
|
563
592
|
def resolved_tools
|
|
564
|
-
|
|
593
|
+
all_tools = if self.class.method_defined?(:tools, false)
|
|
565
594
|
tools
|
|
566
595
|
else
|
|
567
596
|
self.class.tools
|
|
568
597
|
end
|
|
569
598
|
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
wrapped
|
|
573
|
-
end
|
|
574
|
-
|
|
575
|
-
# Wraps an agent class as a tool, or returns the tool class as-is.
|
|
576
|
-
#
|
|
577
|
-
# @param tool_class [Class] A tool or agent class
|
|
578
|
-
# @return [Class] The original or wrapped class
|
|
579
|
-
def wrap_if_agent(tool_class)
|
|
580
|
-
if tool_class.respond_to?(:ancestors) && tool_class.ancestors.include?(RubyLLM::Agents::BaseAgent)
|
|
581
|
-
AgentTool.for(tool_class)
|
|
582
|
-
else
|
|
583
|
-
tool_class
|
|
584
|
-
end
|
|
599
|
+
detect_duplicate_tool_names!(all_tools)
|
|
600
|
+
all_tools
|
|
585
601
|
end
|
|
586
602
|
|
|
587
603
|
# Raises if two tools resolve to the same name.
|
|
@@ -720,9 +736,10 @@ module RubyLLM
|
|
|
720
736
|
# @param context [Pipeline::Context] The execution context
|
|
721
737
|
# @return [void] Sets context.output with the result
|
|
722
738
|
def execute(context)
|
|
739
|
+
@context = context
|
|
723
740
|
client = build_client(context)
|
|
724
741
|
|
|
725
|
-
# Make context available to
|
|
742
|
+
# Make context available to Tool instances during tool execution
|
|
726
743
|
previous_context = Thread.current[:ruby_llm_agents_caller_context]
|
|
727
744
|
Thread.current[:ruby_llm_agents_caller_context] = context
|
|
728
745
|
|
|
@@ -759,9 +776,20 @@ module RubyLLM
|
|
|
759
776
|
end
|
|
760
777
|
client = client.with_temperature(temperature)
|
|
761
778
|
|
|
762
|
-
|
|
779
|
+
use_prompt_caching = self.class.cache_prompts && anthropic_model?(effective_model)
|
|
780
|
+
|
|
781
|
+
if system_prompt
|
|
782
|
+
sys_content = if use_prompt_caching
|
|
783
|
+
RubyLLM::Providers::Anthropic::Content.new(system_prompt, cache: true)
|
|
784
|
+
else
|
|
785
|
+
system_prompt
|
|
786
|
+
end
|
|
787
|
+
client = client.with_instructions(sys_content)
|
|
788
|
+
end
|
|
789
|
+
|
|
763
790
|
client = client.with_schema(schema) if schema
|
|
764
791
|
client = client.with_tools(*resolved_tools) if resolved_tools.any?
|
|
792
|
+
apply_tool_prompt_caching(client) if use_prompt_caching && resolved_tools.any?
|
|
765
793
|
client = setup_tool_tracking(client) if resolved_tools.any?
|
|
766
794
|
client = apply_messages(client, resolved_messages) if resolved_messages.any?
|
|
767
795
|
client = client.with_thinking(**resolved_thinking) if resolved_thinking
|
|
@@ -817,7 +845,11 @@ module RubyLLM
|
|
|
817
845
|
|
|
818
846
|
response = client.complete do |chunk|
|
|
819
847
|
first_chunk_at ||= Time.current
|
|
820
|
-
context.
|
|
848
|
+
if context.stream_events?
|
|
849
|
+
context.stream_block.call(StreamEvent.new(:chunk, {content: chunk.content}))
|
|
850
|
+
else
|
|
851
|
+
context.stream_block.call(chunk)
|
|
852
|
+
end
|
|
821
853
|
end
|
|
822
854
|
|
|
823
855
|
if first_chunk_at
|
|
@@ -843,7 +875,11 @@ module RubyLLM
|
|
|
843
875
|
|
|
844
876
|
response = client.ask(user_prompt, **ask_opts) do |chunk|
|
|
845
877
|
first_chunk_at ||= Time.current
|
|
846
|
-
context.
|
|
878
|
+
if context.stream_events?
|
|
879
|
+
context.stream_block.call(StreamEvent.new(:chunk, {content: chunk.content}))
|
|
880
|
+
else
|
|
881
|
+
context.stream_block.call(chunk)
|
|
882
|
+
end
|
|
847
883
|
end
|
|
848
884
|
|
|
849
885
|
if first_chunk_at
|
|
@@ -867,6 +903,14 @@ module RubyLLM
|
|
|
867
903
|
# Store tracked tool calls in context for instrumentation
|
|
868
904
|
context[:tool_calls] = @tracked_tool_calls if @tracked_tool_calls.any?
|
|
869
905
|
|
|
906
|
+
# Capture Anthropic prompt caching metrics
|
|
907
|
+
if response.respond_to?(:cached_tokens) && response.cached_tokens&.positive?
|
|
908
|
+
context[:cached_tokens] = response.cached_tokens
|
|
909
|
+
end
|
|
910
|
+
if response.respond_to?(:cache_creation_tokens) && response.cache_creation_tokens&.positive?
|
|
911
|
+
context[:cache_creation_tokens] = response.cache_creation_tokens
|
|
912
|
+
end
|
|
913
|
+
|
|
870
914
|
calculate_costs(response, context) if context.input_tokens
|
|
871
915
|
end
|
|
872
916
|
|
|
@@ -901,6 +945,37 @@ module RubyLLM
|
|
|
901
945
|
nil
|
|
902
946
|
end
|
|
903
947
|
|
|
948
|
+
# Checks whether the given model is served by Anthropic
|
|
949
|
+
#
|
|
950
|
+
# Looks up the model's provider in the registry, falling back to
|
|
951
|
+
# model ID pattern matching when the registry is unavailable.
|
|
952
|
+
#
|
|
953
|
+
# @param model_id [String] The model ID
|
|
954
|
+
# @return [Boolean]
|
|
955
|
+
def anthropic_model?(model_id)
|
|
956
|
+
info = find_model_info(model_id)
|
|
957
|
+
return info.provider.to_s == "anthropic" if info&.provider
|
|
958
|
+
|
|
959
|
+
# Fallback: match common Anthropic model ID patterns
|
|
960
|
+
model_id.to_s.match?(/\Aclaude/i)
|
|
961
|
+
end
|
|
962
|
+
|
|
963
|
+
# Adds cache_control to the last tool so Anthropic caches all tool definitions
|
|
964
|
+
#
|
|
965
|
+
# Uses a singleton method override on the last tool instance so the
|
|
966
|
+
# cache_control is merged into the API payload by RubyLLM's
|
|
967
|
+
# Tools.function_for without mutating the tool class.
|
|
968
|
+
#
|
|
969
|
+
# @param client [RubyLLM::Chat] The chat client with tools already added
|
|
970
|
+
def apply_tool_prompt_caching(client)
|
|
971
|
+
last_tool = client.tools.values.last
|
|
972
|
+
return unless last_tool
|
|
973
|
+
|
|
974
|
+
last_tool.define_singleton_method(:provider_params) do
|
|
975
|
+
super().merge(cache_control: {type: "ephemeral"})
|
|
976
|
+
end
|
|
977
|
+
end
|
|
978
|
+
|
|
904
979
|
# Builds a Result object from the response
|
|
905
980
|
#
|
|
906
981
|
# @param content [Object] The processed content
|
|
@@ -993,8 +1068,54 @@ module RubyLLM
|
|
|
993
1068
|
# @return [RubyLLM::Chat] Client with tracking callbacks
|
|
994
1069
|
def setup_tool_tracking(client)
|
|
995
1070
|
client
|
|
996
|
-
.on_tool_call
|
|
997
|
-
|
|
1071
|
+
.on_tool_call do |tool_call|
|
|
1072
|
+
start_tracking_tool_call(tool_call)
|
|
1073
|
+
emit_stream_event(:tool_start, tool_call_start_data(tool_call))
|
|
1074
|
+
end
|
|
1075
|
+
.on_tool_result do |result|
|
|
1076
|
+
end_data = tool_call_end_data(result)
|
|
1077
|
+
complete_tool_call_tracking(result)
|
|
1078
|
+
emit_stream_event(:tool_end, end_data)
|
|
1079
|
+
end
|
|
1080
|
+
end
|
|
1081
|
+
|
|
1082
|
+
# Emits a StreamEvent to the caller's stream block when stream_events is enabled
|
|
1083
|
+
#
|
|
1084
|
+
# @param type [Symbol] Event type (:chunk, :tool_start, :tool_end, :error)
|
|
1085
|
+
# @param data [Hash] Event-specific data
|
|
1086
|
+
def emit_stream_event(type, data)
|
|
1087
|
+
return unless @context&.stream_block && @context.stream_events?
|
|
1088
|
+
|
|
1089
|
+
@context.stream_block.call(StreamEvent.new(type, data))
|
|
1090
|
+
end
|
|
1091
|
+
|
|
1092
|
+
# Builds data hash for a tool_start event
|
|
1093
|
+
#
|
|
1094
|
+
# @param tool_call [Object] The tool call object from RubyLLM
|
|
1095
|
+
# @return [Hash] Event data
|
|
1096
|
+
def tool_call_start_data(tool_call)
|
|
1097
|
+
{
|
|
1098
|
+
tool_name: extract_tool_call_value(tool_call, :name),
|
|
1099
|
+
input: extract_tool_call_value(tool_call, :arguments) || {}
|
|
1100
|
+
}.compact
|
|
1101
|
+
end
|
|
1102
|
+
|
|
1103
|
+
# Builds data hash for a tool_end event from the pending tool call
|
|
1104
|
+
#
|
|
1105
|
+
# @param result [Object] The tool result
|
|
1106
|
+
# @return [Hash] Event data
|
|
1107
|
+
def tool_call_end_data(result)
|
|
1108
|
+
return {} unless @pending_tool_call
|
|
1109
|
+
|
|
1110
|
+
started_at = @pending_tool_call[:started_at]
|
|
1111
|
+
duration_ms = started_at ? ((Time.current - started_at) * 1000).to_i : nil
|
|
1112
|
+
result_data = extract_tool_result(result)
|
|
1113
|
+
|
|
1114
|
+
{
|
|
1115
|
+
tool_name: @pending_tool_call[:name],
|
|
1116
|
+
status: result_data[:status],
|
|
1117
|
+
duration_ms: duration_ms
|
|
1118
|
+
}.compact
|
|
998
1119
|
end
|
|
999
1120
|
|
|
1000
1121
|
# Starts tracking a tool call
|