ruby_llm-agents 3.8.0 → 3.9.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/README.md +30 -10
- data/app/controllers/ruby_llm/agents/requests_controller.rb +117 -0
- data/app/views/layouts/ruby_llm/agents/application.html.erb +4 -2
- data/app/views/ruby_llm/agents/requests/index.html.erb +153 -0
- data/app/views/ruby_llm/agents/requests/show.html.erb +136 -0
- data/config/routes.rb +2 -0
- data/lib/generators/ruby_llm_agents/agent_generator.rb +2 -2
- data/lib/generators/ruby_llm_agents/demo_generator.rb +102 -0
- data/lib/generators/ruby_llm_agents/doctor_generator.rb +196 -0
- data/lib/generators/ruby_llm_agents/install_generator.rb +7 -19
- data/lib/generators/ruby_llm_agents/templates/agent.rb.tt +27 -80
- data/lib/generators/ruby_llm_agents/templates/application_agent.rb.tt +18 -51
- data/lib/generators/ruby_llm_agents/templates/initializer.rb.tt +19 -17
- data/lib/ruby_llm/agents/base_agent.rb +68 -7
- data/lib/ruby_llm/agents/core/base.rb +4 -0
- data/lib/ruby_llm/agents/core/configuration.rb +10 -0
- data/lib/ruby_llm/agents/core/version.rb +1 -1
- data/lib/ruby_llm/agents/pipeline/context.rb +26 -0
- data/lib/ruby_llm/agents/pipeline/middleware/base.rb +58 -4
- data/lib/ruby_llm/agents/pipeline/middleware/budget.rb +17 -15
- data/lib/ruby_llm/agents/pipeline/middleware/cache.rb +34 -22
- data/lib/ruby_llm/agents/pipeline/middleware/instrumentation.rb +105 -50
- data/lib/ruby_llm/agents/pipeline/middleware/reliability.rb +7 -5
- data/lib/ruby_llm/agents/pipeline/middleware/tenant.rb +6 -4
- data/lib/ruby_llm/agents/rails/engine.rb +11 -0
- data/lib/ruby_llm/agents/results/background_removal_result.rb +7 -1
- data/lib/ruby_llm/agents/results/base.rb +24 -2
- data/lib/ruby_llm/agents/results/embedding_result.rb +4 -0
- data/lib/ruby_llm/agents/results/image_analysis_result.rb +7 -1
- data/lib/ruby_llm/agents/results/image_edit_result.rb +7 -1
- data/lib/ruby_llm/agents/results/image_generation_result.rb +7 -1
- data/lib/ruby_llm/agents/results/image_pipeline_result.rb +7 -1
- data/lib/ruby_llm/agents/results/image_transform_result.rb +7 -1
- data/lib/ruby_llm/agents/results/image_upscale_result.rb +7 -1
- data/lib/ruby_llm/agents/results/image_variation_result.rb +7 -1
- data/lib/ruby_llm/agents/results/speech_result.rb +6 -0
- data/lib/ruby_llm/agents/results/trackable.rb +25 -0
- data/lib/ruby_llm/agents/results/transcription_result.rb +6 -0
- data/lib/ruby_llm/agents/text/embedder.rb +7 -4
- data/lib/ruby_llm/agents/track_report.rb +127 -0
- data/lib/ruby_llm/agents/tracker.rb +32 -0
- data/lib/ruby_llm/agents.rb +208 -0
- data/lib/tasks/ruby_llm_agents.rake +6 -0
- metadata +10 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 6925b14509a50c3bbf5efb8d0e37f5a38b4d2f52f13f3c7ecd7af588f832c5c1
|
|
4
|
+
data.tar.gz: 4332d03ebaf4bd94f3e314656e7f6755475c5ad45661a8c1e76e6e324d37ec2e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e12ea68a14b7c9ec683ae7b07a14d9d422922fc8e5233d7296fd42bc6100097bb929da87b6c52fe55929779a17c04e59253fa490c40147558392a32a79023fa6
|
|
7
|
+
data.tar.gz: 9d0d5d249ee0bb6bac741d5c1087ade27ba0a6d032184bb01e1ecfc0d43f79453fbd12cd22e9e4f3eeca7b457bfb242ebca7ebc4e010f44387b9ee8a13bf0865
|
data/README.md
CHANGED
|
@@ -206,7 +206,7 @@ puts run.summary
|
|
|
206
206
|
|
|
207
207
|
## Quick Start
|
|
208
208
|
|
|
209
|
-
###
|
|
209
|
+
### 1. Install
|
|
210
210
|
|
|
211
211
|
```ruby
|
|
212
212
|
# Gemfile
|
|
@@ -219,29 +219,49 @@ rails generate ruby_llm_agents:install
|
|
|
219
219
|
rails db:migrate
|
|
220
220
|
```
|
|
221
221
|
|
|
222
|
-
###
|
|
222
|
+
### 2. Set one API key
|
|
223
223
|
|
|
224
|
-
|
|
224
|
+
Uncomment one line in `config/initializers/ruby_llm_agents.rb`:
|
|
225
225
|
|
|
226
226
|
```ruby
|
|
227
|
-
# config/initializers/ruby_llm_agents.rb
|
|
228
227
|
RubyLLM::Agents.configure do |config|
|
|
229
228
|
config.openai_api_key = ENV["OPENAI_API_KEY"]
|
|
230
|
-
config.anthropic_api_key = ENV["ANTHROPIC_API_KEY"]
|
|
231
|
-
config.gemini_api_key = ENV["GOOGLE_API_KEY"]
|
|
232
229
|
end
|
|
233
230
|
```
|
|
234
231
|
|
|
235
|
-
|
|
232
|
+
Then set the environment variable (e.g., in `.env` or Rails credentials).
|
|
233
|
+
|
|
234
|
+
<details>
|
|
235
|
+
<summary>Other providers</summary>
|
|
236
236
|
|
|
237
|
+
```ruby
|
|
238
|
+
config.anthropic_api_key = ENV["ANTHROPIC_API_KEY"]
|
|
239
|
+
config.gemini_api_key = ENV["GOOGLE_API_KEY"]
|
|
240
|
+
config.deepseek_api_key = ENV["DEEPSEEK_API_KEY"]
|
|
241
|
+
# ... see initializer for full list
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
Or use environment variables directly (auto-detected by RubyLLM):
|
|
237
245
|
```bash
|
|
238
|
-
# .env
|
|
239
246
|
OPENAI_API_KEY=sk-...
|
|
240
247
|
ANTHROPIC_API_KEY=sk-ant-...
|
|
241
|
-
|
|
248
|
+
```
|
|
249
|
+
</details>
|
|
250
|
+
|
|
251
|
+
### 3. Verify setup
|
|
252
|
+
|
|
253
|
+
```bash
|
|
254
|
+
rails ruby_llm_agents:doctor
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
### 4. Try it
|
|
258
|
+
|
|
259
|
+
```bash
|
|
260
|
+
rails generate ruby_llm_agents:demo
|
|
261
|
+
bin/rails runner bin/smoke_test_agent
|
|
242
262
|
```
|
|
243
263
|
|
|
244
|
-
|
|
264
|
+
Or generate a custom agent:
|
|
245
265
|
|
|
246
266
|
```bash
|
|
247
267
|
rails generate ruby_llm_agents:agent SearchIntent query:required
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
module Agents
|
|
5
|
+
# Controller for browsing tracked request groups
|
|
6
|
+
#
|
|
7
|
+
# Provides listing and detail views for executions grouped by
|
|
8
|
+
# request_id, as set by RubyLLM::Agents.track blocks.
|
|
9
|
+
#
|
|
10
|
+
# @api private
|
|
11
|
+
class RequestsController < ApplicationController
|
|
12
|
+
include Paginatable
|
|
13
|
+
|
|
14
|
+
# Lists all tracked requests with aggregated stats
|
|
15
|
+
#
|
|
16
|
+
# @return [void]
|
|
17
|
+
def index
|
|
18
|
+
@sort_column = sanitize_sort_column(params[:sort])
|
|
19
|
+
@sort_direction = (params[:direction] == "asc") ? "asc" : "desc"
|
|
20
|
+
|
|
21
|
+
scope = Execution
|
|
22
|
+
.where.not(request_id: [nil, ""])
|
|
23
|
+
.select(
|
|
24
|
+
"request_id",
|
|
25
|
+
"COUNT(*) AS call_count",
|
|
26
|
+
"SUM(total_cost) AS total_cost",
|
|
27
|
+
"SUM(total_tokens) AS total_tokens",
|
|
28
|
+
"MIN(started_at) AS started_at",
|
|
29
|
+
"MAX(completed_at) AS completed_at",
|
|
30
|
+
"SUM(duration_ms) AS total_duration_ms",
|
|
31
|
+
"GROUP_CONCAT(DISTINCT agent_type) AS agent_types_list",
|
|
32
|
+
"GROUP_CONCAT(DISTINCT status) AS statuses_list",
|
|
33
|
+
"MAX(created_at) AS latest_created_at"
|
|
34
|
+
)
|
|
35
|
+
.group(:request_id)
|
|
36
|
+
|
|
37
|
+
# Apply time filter
|
|
38
|
+
days = params[:days].to_i
|
|
39
|
+
scope = scope.where("created_at >= ?", days.days.ago) if days > 0
|
|
40
|
+
|
|
41
|
+
result = paginate_requests(scope)
|
|
42
|
+
@requests = result[:records]
|
|
43
|
+
@pagination = result[:pagination]
|
|
44
|
+
|
|
45
|
+
# Stats
|
|
46
|
+
total_scope = Execution.where.not(request_id: [nil, ""])
|
|
47
|
+
@stats = {
|
|
48
|
+
total_requests: total_scope.distinct.count(:request_id),
|
|
49
|
+
total_cost: total_scope.sum(:total_cost) || 0
|
|
50
|
+
}
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Shows a single tracked request with all its executions
|
|
54
|
+
#
|
|
55
|
+
# @return [void]
|
|
56
|
+
def show
|
|
57
|
+
@request_id = params[:id]
|
|
58
|
+
@executions = Execution
|
|
59
|
+
.where(request_id: @request_id)
|
|
60
|
+
.order(started_at: :asc)
|
|
61
|
+
|
|
62
|
+
if @executions.empty?
|
|
63
|
+
redirect_to ruby_llm_agents.requests_path,
|
|
64
|
+
alert: "Request not found: #{@request_id}"
|
|
65
|
+
return
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
@summary = {
|
|
69
|
+
call_count: @executions.count,
|
|
70
|
+
total_cost: @executions.sum(:total_cost) || 0,
|
|
71
|
+
total_tokens: @executions.sum(:total_tokens) || 0,
|
|
72
|
+
started_at: @executions.minimum(:started_at),
|
|
73
|
+
completed_at: @executions.maximum(:completed_at),
|
|
74
|
+
agent_types: @executions.distinct.pluck(:agent_type),
|
|
75
|
+
models_used: @executions.distinct.pluck(:model_id),
|
|
76
|
+
all_successful: @executions.where.not(status: "success").count.zero?,
|
|
77
|
+
error_count: @executions.where(status: "error").count
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if @summary[:started_at] && @summary[:completed_at]
|
|
81
|
+
@summary[:duration_ms] = ((@summary[:completed_at] - @summary[:started_at]) * 1000).to_i
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
private
|
|
86
|
+
|
|
87
|
+
ALLOWED_SORT_COLUMNS = %w[latest_created_at call_count total_cost total_tokens total_duration_ms].freeze
|
|
88
|
+
|
|
89
|
+
def sanitize_sort_column(column)
|
|
90
|
+
ALLOWED_SORT_COLUMNS.include?(column) ? column : "latest_created_at"
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def paginate_requests(scope)
|
|
94
|
+
page = [(params[:page] || 1).to_i, 1].max
|
|
95
|
+
per_page = RubyLLM::Agents.configuration.per_page
|
|
96
|
+
|
|
97
|
+
total_count = Execution
|
|
98
|
+
.where.not(request_id: [nil, ""])
|
|
99
|
+
.distinct
|
|
100
|
+
.count(:request_id)
|
|
101
|
+
|
|
102
|
+
sorted = scope.order("#{@sort_column} #{@sort_direction.upcase}")
|
|
103
|
+
offset = (page - 1) * per_page
|
|
104
|
+
|
|
105
|
+
{
|
|
106
|
+
records: sorted.offset(offset).limit(per_page),
|
|
107
|
+
pagination: {
|
|
108
|
+
current_page: page,
|
|
109
|
+
per_page: per_page,
|
|
110
|
+
total_count: total_count,
|
|
111
|
+
total_pages: (total_count.to_f / per_page).ceil
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
@@ -295,7 +295,8 @@
|
|
|
295
295
|
<% nav_items = [
|
|
296
296
|
[ruby_llm_agents.root_path, "dashboard"],
|
|
297
297
|
[ruby_llm_agents.agents_path, "agents"],
|
|
298
|
-
[ruby_llm_agents.executions_path, "executions"]
|
|
298
|
+
[ruby_llm_agents.executions_path, "executions"],
|
|
299
|
+
[ruby_llm_agents.requests_path, "requests"]
|
|
299
300
|
]
|
|
300
301
|
nav_items << [ruby_llm_agents.tenants_path, "tenants"] if tenant_filter_enabled?
|
|
301
302
|
nav_items.each do |path, label| %>
|
|
@@ -345,7 +346,8 @@
|
|
|
345
346
|
<% mobile_nav_items = [
|
|
346
347
|
[ruby_llm_agents.root_path, "dashboard"],
|
|
347
348
|
[ruby_llm_agents.agents_path, "agents"],
|
|
348
|
-
[ruby_llm_agents.executions_path, "executions"]
|
|
349
|
+
[ruby_llm_agents.executions_path, "executions"],
|
|
350
|
+
[ruby_llm_agents.requests_path, "requests"]
|
|
349
351
|
]
|
|
350
352
|
mobile_nav_items << [ruby_llm_agents.tenants_path, "tenants"] if tenant_filter_enabled?
|
|
351
353
|
mobile_nav_items.each do |path, label| %>
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
<div class="flex items-center gap-3 mb-6">
|
|
2
|
+
<span class="text-[10px] font-medium text-gray-400 dark:text-gray-600 uppercase tracking-widest font-mono">requests</span>
|
|
3
|
+
<div class="flex-1 border-t border-gray-200 dark:border-gray-800"></div>
|
|
4
|
+
<span class="font-mono text-[10px] text-gray-400 dark:text-gray-600">
|
|
5
|
+
<%= number_with_delimiter(@stats[:total_requests]) %> tracked
|
|
6
|
+
·
|
|
7
|
+
$<%= number_with_precision(@stats[:total_cost] || 0, precision: 4) %> total
|
|
8
|
+
</span>
|
|
9
|
+
</div>
|
|
10
|
+
|
|
11
|
+
<%
|
|
12
|
+
sort_column = @sort_column
|
|
13
|
+
sort_direction = @sort_direction
|
|
14
|
+
|
|
15
|
+
sort_link = ->(column, label, extra_class: "") {
|
|
16
|
+
is_active = column == sort_column
|
|
17
|
+
next_dir = is_active && sort_direction == "asc" ? "desc" : "asc"
|
|
18
|
+
url = url_for(request.query_parameters.merge(sort: column, direction: next_dir, page: 1))
|
|
19
|
+
|
|
20
|
+
arrow = if is_active && sort_direction == "asc"
|
|
21
|
+
raw('<svg class="w-2.5 h-2.5 inline" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 15l7-7 7 7"/></svg>')
|
|
22
|
+
elsif is_active
|
|
23
|
+
raw('<svg class="w-2.5 h-2.5 inline" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/></svg>')
|
|
24
|
+
else
|
|
25
|
+
""
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
active_class = is_active ? "text-gray-700 dark:text-gray-300" : ""
|
|
29
|
+
raw(%(<a href="#{url}" class="group inline-flex items-center gap-0.5 hover:text-gray-700 dark:hover:text-gray-300 #{active_class} #{extra_class}"><span>#{label}</span><span class="#{is_active ? 'opacity-100' : 'opacity-0 group-hover:opacity-50'} transition-opacity">#{arrow}</span></a>))
|
|
30
|
+
}
|
|
31
|
+
%>
|
|
32
|
+
|
|
33
|
+
<% if @requests.empty? %>
|
|
34
|
+
<div class="font-mono text-xs text-gray-400 dark:text-gray-600 py-8 text-center">
|
|
35
|
+
No tracked requests found. Use <code class="bg-gray-100 dark:bg-gray-800 px-1.5 py-0.5 rounded">RubyLLM::Agents.track { ... }</code> to start tracking.
|
|
36
|
+
</div>
|
|
37
|
+
<% else %>
|
|
38
|
+
<!-- Column headers -->
|
|
39
|
+
<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">
|
|
40
|
+
<span class="w-36 flex-shrink-0">request_id</span>
|
|
41
|
+
<span class="flex-1 min-w-0">agents</span>
|
|
42
|
+
<span class="w-12 flex-shrink-0 text-right"><%= sort_link.call("call_count", "calls", extra_class: "justify-end w-full") %></span>
|
|
43
|
+
<span class="w-14 flex-shrink-0 text-right hidden sm:block">status</span>
|
|
44
|
+
<span class="w-16 flex-shrink-0 text-right hidden md:block"><%= sort_link.call("total_duration_ms", "duration", extra_class: "justify-end w-full") %></span>
|
|
45
|
+
<span class="w-16 flex-shrink-0 text-right hidden md:block"><%= sort_link.call("total_tokens", "tokens", extra_class: "justify-end w-full") %></span>
|
|
46
|
+
<span class="w-16 flex-shrink-0 text-right"><%= sort_link.call("total_cost", "cost", extra_class: "justify-end w-full") %></span>
|
|
47
|
+
<span class="w-24 flex-shrink-0 text-right"><%= sort_link.call("latest_created_at", "time", extra_class: "justify-end w-full") %></span>
|
|
48
|
+
</div>
|
|
49
|
+
|
|
50
|
+
<!-- Rows -->
|
|
51
|
+
<div class="font-mono text-xs space-y-px">
|
|
52
|
+
<% @requests.each do |req| %>
|
|
53
|
+
<%
|
|
54
|
+
statuses = (req.statuses_list || "").split(",")
|
|
55
|
+
has_errors = statuses.include?("error") || statuses.include?("timeout")
|
|
56
|
+
all_success = statuses == ["success"]
|
|
57
|
+
status_class = if has_errors
|
|
58
|
+
"badge-error"
|
|
59
|
+
elsif all_success
|
|
60
|
+
"badge-success"
|
|
61
|
+
else
|
|
62
|
+
"badge-running"
|
|
63
|
+
end
|
|
64
|
+
status_label = if has_errors
|
|
65
|
+
"errors"
|
|
66
|
+
elsif all_success
|
|
67
|
+
"ok"
|
|
68
|
+
else
|
|
69
|
+
"mixed"
|
|
70
|
+
end
|
|
71
|
+
agent_names = (req.agent_types_list || "").split(",").map { |a| a.gsub(/Agent$/, "") }
|
|
72
|
+
%>
|
|
73
|
+
<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"
|
|
74
|
+
onclick="window.location='<%= ruby_llm_agents.request_path(req.request_id) %>'">
|
|
75
|
+
<span class="w-36 flex-shrink-0 truncate text-gray-900 dark:text-gray-200" title="<%= req.request_id %>">
|
|
76
|
+
<%= truncate(req.request_id, length: 20) %>
|
|
77
|
+
</span>
|
|
78
|
+
<span class="flex-1 min-w-0 truncate text-gray-400 dark:text-gray-600">
|
|
79
|
+
<%= agent_names.first(3).join(", ") %><%= agent_names.size > 3 ? " +#{agent_names.size - 3}" : "" %>
|
|
80
|
+
</span>
|
|
81
|
+
<span class="w-12 flex-shrink-0 text-right text-gray-500 dark:text-gray-400"><%= req.call_count %></span>
|
|
82
|
+
<span class="w-14 flex-shrink-0 text-right hidden sm:block">
|
|
83
|
+
<span class="badge badge-sm <%= status_class %>"><%= status_label %></span>
|
|
84
|
+
</span>
|
|
85
|
+
<span class="w-16 flex-shrink-0 text-right text-gray-500 dark:text-gray-400 hidden md:inline">
|
|
86
|
+
<%= req.total_duration_ms ? format_duration_ms(req.total_duration_ms.to_i) : "—" %>
|
|
87
|
+
</span>
|
|
88
|
+
<span class="w-16 flex-shrink-0 text-right text-gray-500 dark:text-gray-400 hidden md:inline"><%= number_with_delimiter(req.total_tokens || 0) %></span>
|
|
89
|
+
<span class="w-16 flex-shrink-0 text-right text-gray-500 dark:text-gray-400">$<%= number_with_precision(req.total_cost || 0, precision: 4) %></span>
|
|
90
|
+
<span class="w-24 flex-shrink-0 text-gray-400 dark:text-gray-600 text-right whitespace-nowrap">
|
|
91
|
+
<% if req.respond_to?(:latest_created_at) && req.latest_created_at %>
|
|
92
|
+
<%= time_ago_in_words(req.latest_created_at) %>
|
|
93
|
+
<% else %>
|
|
94
|
+
—
|
|
95
|
+
<% end %>
|
|
96
|
+
</span>
|
|
97
|
+
</div>
|
|
98
|
+
<% end %>
|
|
99
|
+
</div>
|
|
100
|
+
|
|
101
|
+
<%# Pagination %>
|
|
102
|
+
<% if @pagination && @pagination[:total_pages] > 1 %>
|
|
103
|
+
<%
|
|
104
|
+
current_page = @pagination[:current_page]
|
|
105
|
+
total_pages = @pagination[:total_pages]
|
|
106
|
+
total_count = @pagination[:total_count]
|
|
107
|
+
per_page = @pagination[:per_page]
|
|
108
|
+
from_record = ((current_page - 1) * per_page) + 1
|
|
109
|
+
to_record = [current_page * per_page, total_count].min
|
|
110
|
+
%>
|
|
111
|
+
<div class="flex items-center justify-between font-mono text-xs mt-4 pt-4 border-t border-gray-200 dark:border-gray-800">
|
|
112
|
+
<span class="text-gray-400 dark:text-gray-600"><%= from_record %>-<%= to_record %> of <%= number_with_delimiter(total_count) %></span>
|
|
113
|
+
<nav class="flex items-center gap-1">
|
|
114
|
+
<% if current_page > 1 %>
|
|
115
|
+
<%= link_to "prev", url_for(request.query_parameters.merge(page: current_page - 1)),
|
|
116
|
+
class: "px-2 py-0.5 text-gray-400 dark:text-gray-500 hover:text-gray-700 dark:hover:text-gray-300" %>
|
|
117
|
+
<% else %>
|
|
118
|
+
<span class="px-2 py-0.5 text-gray-300 dark:text-gray-700">prev</span>
|
|
119
|
+
<% end %>
|
|
120
|
+
|
|
121
|
+
<%
|
|
122
|
+
window = 2
|
|
123
|
+
pages_to_show = []
|
|
124
|
+
(1..total_pages).each do |page|
|
|
125
|
+
if page <= 1 || page >= total_pages || (page >= current_page - window && page <= current_page + window)
|
|
126
|
+
pages_to_show << page
|
|
127
|
+
elsif pages_to_show.last != :gap
|
|
128
|
+
pages_to_show << :gap
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
%>
|
|
132
|
+
|
|
133
|
+
<% pages_to_show.each do |page| %>
|
|
134
|
+
<% if page == :gap %>
|
|
135
|
+
<span class="px-1 text-gray-400 dark:text-gray-600">...</span>
|
|
136
|
+
<% elsif page == current_page %>
|
|
137
|
+
<span class="px-2 py-0.5 text-gray-900 dark:text-gray-100"><%= page %></span>
|
|
138
|
+
<% else %>
|
|
139
|
+
<%= link_to page, url_for(request.query_parameters.merge(page: page)),
|
|
140
|
+
class: "px-2 py-0.5 text-gray-400 dark:text-gray-500 hover:text-gray-700 dark:hover:text-gray-300" %>
|
|
141
|
+
<% end %>
|
|
142
|
+
<% end %>
|
|
143
|
+
|
|
144
|
+
<% if current_page < total_pages %>
|
|
145
|
+
<%= link_to "next", url_for(request.query_parameters.merge(page: current_page + 1)),
|
|
146
|
+
class: "px-2 py-0.5 text-gray-400 dark:text-gray-500 hover:text-gray-700 dark:hover:text-gray-300" %>
|
|
147
|
+
<% else %>
|
|
148
|
+
<span class="px-2 py-0.5 text-gray-300 dark:text-gray-700">next</span>
|
|
149
|
+
<% end %>
|
|
150
|
+
</nav>
|
|
151
|
+
</div>
|
|
152
|
+
<% end %>
|
|
153
|
+
<% end %>
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
<div class="flex items-center gap-3 mb-6">
|
|
2
|
+
<%= link_to ruby_llm_agents.requests_path, class: "text-gray-400 dark:text-gray-600 hover:text-gray-700 dark:hover:text-gray-300 transition-colors" do %>
|
|
3
|
+
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
|
|
4
|
+
<path stroke-linecap="round" stroke-linejoin="round" d="M15 19l-7-7 7-7"/>
|
|
5
|
+
</svg>
|
|
6
|
+
<% end %>
|
|
7
|
+
<span class="text-[10px] font-medium text-gray-400 dark:text-gray-600 uppercase tracking-widest font-mono">request</span>
|
|
8
|
+
<code class="font-mono text-xs text-gray-900 dark:text-gray-200 bg-gray-100 dark:bg-gray-800 px-2 py-0.5 rounded"><%= @request_id %></code>
|
|
9
|
+
<div class="flex-1 border-t border-gray-200 dark:border-gray-800"></div>
|
|
10
|
+
<% if @summary[:all_successful] %>
|
|
11
|
+
<span class="badge badge-sm badge-success">all ok</span>
|
|
12
|
+
<% elsif @summary[:error_count] > 0 %>
|
|
13
|
+
<span class="badge badge-sm badge-error"><%= @summary[:error_count] %> error<%= @summary[:error_count] > 1 ? "s" : "" %></span>
|
|
14
|
+
<% end %>
|
|
15
|
+
</div>
|
|
16
|
+
|
|
17
|
+
<!-- Summary Stats -->
|
|
18
|
+
<div class="grid grid-cols-2 sm:grid-cols-4 gap-4 mb-6">
|
|
19
|
+
<div class="bg-gray-50 dark:bg-gray-800/50 rounded-lg px-4 py-3">
|
|
20
|
+
<div class="font-mono text-[10px] text-gray-400 dark:text-gray-600 uppercase tracking-wider mb-1">calls</div>
|
|
21
|
+
<div class="font-mono text-lg text-gray-900 dark:text-gray-100"><%= @summary[:call_count] %></div>
|
|
22
|
+
</div>
|
|
23
|
+
<div class="bg-gray-50 dark:bg-gray-800/50 rounded-lg px-4 py-3">
|
|
24
|
+
<div class="font-mono text-[10px] text-gray-400 dark:text-gray-600 uppercase tracking-wider mb-1">total cost</div>
|
|
25
|
+
<div class="font-mono text-lg text-gray-900 dark:text-gray-100">$<%= number_with_precision(@summary[:total_cost] || 0, precision: 4) %></div>
|
|
26
|
+
</div>
|
|
27
|
+
<div class="bg-gray-50 dark:bg-gray-800/50 rounded-lg px-4 py-3">
|
|
28
|
+
<div class="font-mono text-[10px] text-gray-400 dark:text-gray-600 uppercase tracking-wider mb-1">tokens</div>
|
|
29
|
+
<div class="font-mono text-lg text-gray-900 dark:text-gray-100"><%= number_with_delimiter(@summary[:total_tokens] || 0) %></div>
|
|
30
|
+
</div>
|
|
31
|
+
<div class="bg-gray-50 dark:bg-gray-800/50 rounded-lg px-4 py-3">
|
|
32
|
+
<div class="font-mono text-[10px] text-gray-400 dark:text-gray-600 uppercase tracking-wider mb-1">duration</div>
|
|
33
|
+
<div class="font-mono text-lg text-gray-900 dark:text-gray-100">
|
|
34
|
+
<%= @summary[:duration_ms] ? format_duration_ms(@summary[:duration_ms]) : "—" %>
|
|
35
|
+
</div>
|
|
36
|
+
</div>
|
|
37
|
+
</div>
|
|
38
|
+
|
|
39
|
+
<!-- Agents & Models -->
|
|
40
|
+
<div class="flex flex-wrap gap-2 mb-6">
|
|
41
|
+
<% @summary[:agent_types].each do |agent_type| %>
|
|
42
|
+
<%= link_to agent_type.gsub(/Agent$/, ""),
|
|
43
|
+
ruby_llm_agents.agent_path(agent_type),
|
|
44
|
+
class: "font-mono text-xs px-2 py-0.5 rounded bg-gray-100 dark:bg-gray-800 text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200 transition-colors" %>
|
|
45
|
+
<% end %>
|
|
46
|
+
<span class="w-px h-4 bg-gray-200 dark:bg-gray-700 self-center"></span>
|
|
47
|
+
<% @summary[:models_used].each do |model| %>
|
|
48
|
+
<span class="font-mono text-xs px-2 py-0.5 rounded bg-gray-50 dark:bg-gray-800/30 text-gray-400 dark:text-gray-600"><%= model %></span>
|
|
49
|
+
<% end %>
|
|
50
|
+
</div>
|
|
51
|
+
|
|
52
|
+
<!-- Execution Timeline -->
|
|
53
|
+
<div class="mb-2">
|
|
54
|
+
<span class="text-[10px] font-medium text-gray-400 dark:text-gray-600 uppercase tracking-widest font-mono">execution timeline</span>
|
|
55
|
+
</div>
|
|
56
|
+
|
|
57
|
+
<div class="space-y-px">
|
|
58
|
+
<!-- Column headers -->
|
|
59
|
+
<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">
|
|
60
|
+
<span class="w-5 flex-shrink-0 text-center">#</span>
|
|
61
|
+
<span class="flex-1 min-w-0">agent</span>
|
|
62
|
+
<span class="w-20 flex-shrink-0">status</span>
|
|
63
|
+
<span class="flex-1 min-w-0 hidden lg:block">model</span>
|
|
64
|
+
<span class="w-16 flex-shrink-0 text-right">duration</span>
|
|
65
|
+
<span class="w-16 flex-shrink-0 text-right hidden md:block">tokens</span>
|
|
66
|
+
<span class="w-16 flex-shrink-0 text-right hidden md:block">cost</span>
|
|
67
|
+
</div>
|
|
68
|
+
|
|
69
|
+
<div class="font-mono text-xs">
|
|
70
|
+
<% @executions.each_with_index do |execution, idx| %>
|
|
71
|
+
<%
|
|
72
|
+
status_badge_class = case execution.status.to_s
|
|
73
|
+
when "running" then "badge-running"
|
|
74
|
+
when "success" then "badge-success"
|
|
75
|
+
when "error" then "badge-error"
|
|
76
|
+
when "timeout" then "badge-timeout"
|
|
77
|
+
else "badge-timeout"
|
|
78
|
+
end
|
|
79
|
+
%>
|
|
80
|
+
<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"
|
|
81
|
+
onclick="window.location='<%= ruby_llm_agents.execution_path(execution) %>'">
|
|
82
|
+
<span class="w-5 flex-shrink-0 text-center text-gray-300 dark:text-gray-700"><%= idx + 1 %></span>
|
|
83
|
+
<span class="flex-1 min-w-0 truncate">
|
|
84
|
+
<%= link_to execution.agent_type.gsub(/Agent$/, ""),
|
|
85
|
+
ruby_llm_agents.agent_path(execution.agent_type),
|
|
86
|
+
class: "text-gray-900 dark:text-gray-200 hover:underline",
|
|
87
|
+
onclick: "event.stopPropagation()" %>
|
|
88
|
+
</span>
|
|
89
|
+
<span class="w-20 flex-shrink-0">
|
|
90
|
+
<span class="badge badge-sm <%= status_badge_class %>"><%= execution.status %></span>
|
|
91
|
+
</span>
|
|
92
|
+
<span class="flex-1 min-w-0 truncate text-gray-400 dark:text-gray-600 hidden lg:inline" title="<%= execution.model_id %>"><%= execution.model_id %></span>
|
|
93
|
+
<span class="w-16 flex-shrink-0 text-right text-gray-500 dark:text-gray-400">
|
|
94
|
+
<% if execution.status_running? %>
|
|
95
|
+
<span class="text-blue-500 animate-pulse">...</span>
|
|
96
|
+
<% else %>
|
|
97
|
+
<%= execution.duration_ms ? format_duration_ms(execution.duration_ms) : "—" %>
|
|
98
|
+
<% end %>
|
|
99
|
+
</span>
|
|
100
|
+
<span class="w-16 flex-shrink-0 text-right text-gray-500 dark:text-gray-400 hidden md:inline"><%= number_with_delimiter(execution.total_tokens || 0) %></span>
|
|
101
|
+
<span class="w-16 flex-shrink-0 text-right text-gray-500 dark:text-gray-400 hidden md:inline">$<%= number_with_precision(execution.total_cost || 0, precision: 4) %></span>
|
|
102
|
+
</div>
|
|
103
|
+
<% if execution.status_error? && execution.error_class.present? %>
|
|
104
|
+
<div class="flex items-center gap-1 pl-8 py-0.5 text-red-400 dark:text-red-500/70 text-xs font-mono">
|
|
105
|
+
<span class="text-gray-300 dark:text-gray-700">└</span>
|
|
106
|
+
<span class="truncate"><%= execution.error_class.split("::").last %><%= execution.error_message.present? ? ": #{truncate(execution.error_message, length: 100)}" : "" %></span>
|
|
107
|
+
</div>
|
|
108
|
+
<% end %>
|
|
109
|
+
<% end %>
|
|
110
|
+
</div>
|
|
111
|
+
</div>
|
|
112
|
+
|
|
113
|
+
<!-- Timing Details -->
|
|
114
|
+
<% if @summary[:started_at] %>
|
|
115
|
+
<div class="mt-6 pt-4 border-t border-gray-200 dark:border-gray-800">
|
|
116
|
+
<span class="text-[10px] font-medium text-gray-400 dark:text-gray-600 uppercase tracking-widest font-mono">timing</span>
|
|
117
|
+
<div class="mt-2 font-mono text-xs text-gray-500 dark:text-gray-400 space-y-1">
|
|
118
|
+
<div class="flex gap-4">
|
|
119
|
+
<span class="text-gray-400 dark:text-gray-600 w-20">started</span>
|
|
120
|
+
<span><%= @summary[:started_at].strftime("%Y-%m-%d %H:%M:%S.%L") %></span>
|
|
121
|
+
</div>
|
|
122
|
+
<% if @summary[:completed_at] %>
|
|
123
|
+
<div class="flex gap-4">
|
|
124
|
+
<span class="text-gray-400 dark:text-gray-600 w-20">completed</span>
|
|
125
|
+
<span><%= @summary[:completed_at].strftime("%Y-%m-%d %H:%M:%S.%L") %></span>
|
|
126
|
+
</div>
|
|
127
|
+
<% end %>
|
|
128
|
+
<% if @summary[:duration_ms] %>
|
|
129
|
+
<div class="flex gap-4">
|
|
130
|
+
<span class="text-gray-400 dark:text-gray-600 w-20">wall clock</span>
|
|
131
|
+
<span><%= format_duration_ms(@summary[:duration_ms]) %></span>
|
|
132
|
+
</div>
|
|
133
|
+
<% end %>
|
|
134
|
+
</div>
|
|
135
|
+
</div>
|
|
136
|
+
<% end %>
|
data/config/routes.rb
CHANGED
|
@@ -21,8 +21,8 @@ module RubyLlmAgents
|
|
|
21
21
|
|
|
22
22
|
argument :params, type: :array, default: [], banner: "param[:required|:default] param[:required|:default]"
|
|
23
23
|
|
|
24
|
-
class_option :model, type: :string, default: "
|
|
25
|
-
desc: "The LLM model to use"
|
|
24
|
+
class_option :model, type: :string, default: "default",
|
|
25
|
+
desc: "The LLM model to use (omit to use configured default)"
|
|
26
26
|
class_option :temperature, type: :numeric, default: 0.0,
|
|
27
27
|
desc: "The temperature setting (0.0-1.0)"
|
|
28
28
|
class_option :cache, type: :string, default: nil,
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rails/generators"
|
|
4
|
+
|
|
5
|
+
module RubyLlmAgents
|
|
6
|
+
# Demo generator — scaffolds a working HelloAgent with a smoke-test script.
|
|
7
|
+
#
|
|
8
|
+
# Usage:
|
|
9
|
+
# rails generate ruby_llm_agents:demo
|
|
10
|
+
#
|
|
11
|
+
# Creates:
|
|
12
|
+
# - app/agents/hello_agent.rb — minimal working agent
|
|
13
|
+
# - bin/smoke_test_agent — one-command verification script
|
|
14
|
+
#
|
|
15
|
+
class DemoGenerator < ::Rails::Generators::Base
|
|
16
|
+
source_root File.expand_path("templates", __dir__)
|
|
17
|
+
|
|
18
|
+
def ensure_base_class
|
|
19
|
+
agents_dir = "app/agents"
|
|
20
|
+
empty_directory agents_dir
|
|
21
|
+
|
|
22
|
+
base_class_path = "#{agents_dir}/application_agent.rb"
|
|
23
|
+
unless File.exist?(File.join(destination_root, base_class_path))
|
|
24
|
+
template "application_agent.rb.tt", base_class_path
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def create_hello_agent
|
|
29
|
+
create_file "app/agents/hello_agent.rb", <<~RUBY
|
|
30
|
+
# frozen_string_literal: true
|
|
31
|
+
|
|
32
|
+
class HelloAgent < ApplicationAgent
|
|
33
|
+
system "You are a friendly assistant. Keep responses under 2 sentences."
|
|
34
|
+
|
|
35
|
+
prompt "Say hello to {name} and tell them one fun fact."
|
|
36
|
+
end
|
|
37
|
+
RUBY
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def create_smoke_test
|
|
41
|
+
create_file "bin/smoke_test_agent", <<~RUBY
|
|
42
|
+
#!/usr/bin/env ruby
|
|
43
|
+
# frozen_string_literal: true
|
|
44
|
+
|
|
45
|
+
# Smoke test — verifies your RubyLLM::Agents setup end-to-end.
|
|
46
|
+
#
|
|
47
|
+
# Usage:
|
|
48
|
+
# bin/rails runner bin/smoke_test_agent
|
|
49
|
+
#
|
|
50
|
+
puts "Running RubyLLM::Agents smoke test..."
|
|
51
|
+
puts ""
|
|
52
|
+
|
|
53
|
+
# 1. Check configuration
|
|
54
|
+
config = RubyLLM::Agents.configuration
|
|
55
|
+
model = config.default_model
|
|
56
|
+
puts "Default model: \#{model}"
|
|
57
|
+
|
|
58
|
+
# 2. Dry-run (no API call)
|
|
59
|
+
puts ""
|
|
60
|
+
puts "Dry run:"
|
|
61
|
+
dry = HelloAgent.call(name: "World", dry_run: true)
|
|
62
|
+
puts " System prompt: \#{dry.system_prompt[0..80]}..."
|
|
63
|
+
puts " User prompt: \#{dry.user_prompt}"
|
|
64
|
+
puts " Model: \#{dry.model}"
|
|
65
|
+
puts " Dry run OK!"
|
|
66
|
+
|
|
67
|
+
# 3. Live call
|
|
68
|
+
puts ""
|
|
69
|
+
puts "Live call (calling \#{model})..."
|
|
70
|
+
begin
|
|
71
|
+
result = HelloAgent.call(name: "World")
|
|
72
|
+
puts " Response: \#{result.content}"
|
|
73
|
+
puts ""
|
|
74
|
+
puts "Success! Your setup is working."
|
|
75
|
+
rescue => e
|
|
76
|
+
puts " Error: \#{e.class}: \#{e.message}"
|
|
77
|
+
puts ""
|
|
78
|
+
puts "The dry run worked but the live call failed."
|
|
79
|
+
puts "This usually means your API key is missing or invalid."
|
|
80
|
+
puts ""
|
|
81
|
+
puts "Run 'rails ruby_llm_agents:doctor' for detailed diagnostics."
|
|
82
|
+
exit 1
|
|
83
|
+
end
|
|
84
|
+
RUBY
|
|
85
|
+
|
|
86
|
+
chmod "bin/smoke_test_agent", 0o755
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def show_next_steps
|
|
90
|
+
say ""
|
|
91
|
+
say "Demo agent created!", :green
|
|
92
|
+
say ""
|
|
93
|
+
say "Try it:"
|
|
94
|
+
say " bin/rails runner bin/smoke_test_agent"
|
|
95
|
+
say ""
|
|
96
|
+
say "Or in the Rails console:"
|
|
97
|
+
say " HelloAgent.call(name: \"World\")"
|
|
98
|
+
say " HelloAgent.call(name: \"World\", dry_run: true)"
|
|
99
|
+
say ""
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|