ruby_llm-agents 0.1.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 +7 -0
- data/LICENSE.txt +21 -0
- data/README.md +898 -0
- data/app/channels/ruby_llm/agents/executions_channel.rb +23 -0
- data/app/controllers/ruby_llm/agents/agents_controller.rb +100 -0
- data/app/controllers/ruby_llm/agents/application_controller.rb +20 -0
- data/app/controllers/ruby_llm/agents/dashboard_controller.rb +34 -0
- data/app/controllers/ruby_llm/agents/executions_controller.rb +93 -0
- data/app/helpers/ruby_llm/agents/application_helper.rb +149 -0
- data/app/javascript/ruby_llm/agents/controllers/filter_controller.js +56 -0
- data/app/javascript/ruby_llm/agents/controllers/index.js +12 -0
- data/app/javascript/ruby_llm/agents/controllers/refresh_controller.js +83 -0
- data/app/models/ruby_llm/agents/execution/analytics.rb +166 -0
- data/app/models/ruby_llm/agents/execution/metrics.rb +89 -0
- data/app/models/ruby_llm/agents/execution/scopes.rb +81 -0
- data/app/models/ruby_llm/agents/execution.rb +81 -0
- data/app/services/ruby_llm/agents/agent_registry.rb +112 -0
- data/app/views/layouts/rubyllm/agents/application.html.erb +276 -0
- data/app/views/rubyllm/agents/agents/index.html.erb +89 -0
- data/app/views/rubyllm/agents/agents/show.html.erb +562 -0
- data/app/views/rubyllm/agents/dashboard/_execution_item.html.erb +48 -0
- data/app/views/rubyllm/agents/dashboard/index.html.erb +121 -0
- data/app/views/rubyllm/agents/executions/_execution.html.erb +64 -0
- data/app/views/rubyllm/agents/executions/_filters.html.erb +172 -0
- data/app/views/rubyllm/agents/executions/_list.html.erb +229 -0
- data/app/views/rubyllm/agents/executions/index.html.erb +83 -0
- data/app/views/rubyllm/agents/executions/index.turbo_stream.erb +4 -0
- data/app/views/rubyllm/agents/executions/show.html.erb +240 -0
- data/app/views/rubyllm/agents/shared/_executions_table.html.erb +193 -0
- data/app/views/rubyllm/agents/shared/_stat_card.html.erb +14 -0
- data/app/views/rubyllm/agents/shared/_status_badge.html.erb +65 -0
- data/app/views/rubyllm/agents/shared/_status_dot.html.erb +18 -0
- data/config/routes.rb +13 -0
- data/lib/generators/ruby_llm_agents/agent_generator.rb +79 -0
- data/lib/generators/ruby_llm_agents/install_generator.rb +89 -0
- data/lib/generators/ruby_llm_agents/templates/add_prompts_migration.rb.tt +12 -0
- data/lib/generators/ruby_llm_agents/templates/agent.rb.tt +46 -0
- data/lib/generators/ruby_llm_agents/templates/application_agent.rb.tt +22 -0
- data/lib/generators/ruby_llm_agents/templates/initializer.rb.tt +36 -0
- data/lib/generators/ruby_llm_agents/templates/migration.rb.tt +66 -0
- data/lib/generators/ruby_llm_agents/upgrade_generator.rb +59 -0
- data/lib/ruby_llm/agents/base.rb +271 -0
- data/lib/ruby_llm/agents/configuration.rb +36 -0
- data/lib/ruby_llm/agents/engine.rb +32 -0
- data/lib/ruby_llm/agents/execution_logger_job.rb +59 -0
- data/lib/ruby_llm/agents/inflections.rb +13 -0
- data/lib/ruby_llm/agents/instrumentation.rb +245 -0
- data/lib/ruby_llm/agents/version.rb +7 -0
- data/lib/ruby_llm/agents.rb +26 -0
- data/lib/ruby_llm-agents.rb +3 -0
- metadata +164 -0
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
<%= turbo_frame_tag "dashboard", data: { connection_target: "dashboardFrame" } do %>
|
|
2
|
+
<!-- Stats Cards -->
|
|
3
|
+
<div class="grid grid-cols-2 lg:grid-cols-4 gap-4 mb-6">
|
|
4
|
+
<%= render "rubyllm/agents/shared/stat_card",
|
|
5
|
+
title: "Executions",
|
|
6
|
+
value: number_with_delimiter(@stats[:total_executions]),
|
|
7
|
+
icon: "M13 10V3L4 14h7v7l9-11h-7z",
|
|
8
|
+
icon_color: "text-blue-500" %>
|
|
9
|
+
|
|
10
|
+
<%= render "rubyllm/agents/shared/stat_card",
|
|
11
|
+
title: "Failed",
|
|
12
|
+
value: number_with_delimiter(@stats[:failed]),
|
|
13
|
+
icon: "M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z",
|
|
14
|
+
icon_color: "text-red-500",
|
|
15
|
+
value_color: "text-red-600" %>
|
|
16
|
+
|
|
17
|
+
<%= render "rubyllm/agents/shared/stat_card",
|
|
18
|
+
title: "Total Cost",
|
|
19
|
+
value: "$#{number_with_precision(@stats[:total_cost], precision: 4)}",
|
|
20
|
+
icon: "M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1M21 12a9 9 0 11-18 0 9 9 0 0118 0z",
|
|
21
|
+
icon_color: "text-amber-500" %>
|
|
22
|
+
|
|
23
|
+
<%= render "rubyllm/agents/shared/stat_card",
|
|
24
|
+
title: "Total Tokens",
|
|
25
|
+
value: number_with_delimiter(@stats[:total_tokens]),
|
|
26
|
+
icon: "M7 20l4-16m2 16l4-16M6 9h14M4 15h14",
|
|
27
|
+
icon_color: "text-indigo-500" %>
|
|
28
|
+
</div>
|
|
29
|
+
|
|
30
|
+
<!-- Activity Chart -->
|
|
31
|
+
<div class="bg-white rounded-xl shadow-sm border border-gray-100 mb-8 pb-24">
|
|
32
|
+
<div class="px-6 py-4 border-b border-gray-100">
|
|
33
|
+
<div class="flex justify-between items-center">
|
|
34
|
+
<h3 class="text-lg font-semibold text-gray-900">Today's Activity</h3>
|
|
35
|
+
<span class="text-xs text-gray-400">Hourly executions</span>
|
|
36
|
+
</div>
|
|
37
|
+
</div>
|
|
38
|
+
|
|
39
|
+
<div class="p-6">
|
|
40
|
+
<div style="height: 200px;">
|
|
41
|
+
<%= area_chart @hourly_activity,
|
|
42
|
+
stacked: true,
|
|
43
|
+
colors: ["#10B981", "#EF4444"],
|
|
44
|
+
library: {
|
|
45
|
+
maintainAspectRatio: false,
|
|
46
|
+
scales: {
|
|
47
|
+
y: { beginAtZero: true, ticks: { stepSize: 1 } }
|
|
48
|
+
},
|
|
49
|
+
plugins: {
|
|
50
|
+
legend: { position: "bottom" }
|
|
51
|
+
}
|
|
52
|
+
} %>
|
|
53
|
+
</div>
|
|
54
|
+
</div>
|
|
55
|
+
</div>
|
|
56
|
+
|
|
57
|
+
<!-- Activity Feed -->
|
|
58
|
+
<div class="bg-white rounded-xl shadow-sm border border-gray-100">
|
|
59
|
+
<div class="px-6 py-4 border-b border-gray-100">
|
|
60
|
+
<div class="flex justify-between items-center">
|
|
61
|
+
<div class="flex items-center space-x-3">
|
|
62
|
+
<h3 class="text-lg font-semibold text-gray-900">Activity Feed</h3>
|
|
63
|
+
|
|
64
|
+
<div class="flex items-center space-x-3 text-xs text-gray-400">
|
|
65
|
+
<span class="flex items-center">
|
|
66
|
+
<span class="w-2 h-2 bg-blue-500 rounded-full mr-1 animate-pulse"></span>
|
|
67
|
+
running
|
|
68
|
+
</span>
|
|
69
|
+
|
|
70
|
+
<span class="flex items-center">
|
|
71
|
+
<span class="w-2 h-2 bg-green-500 rounded-full mr-1"></span>
|
|
72
|
+
success
|
|
73
|
+
</span>
|
|
74
|
+
|
|
75
|
+
<span class="flex items-center">
|
|
76
|
+
<span class="w-2 h-2 bg-red-500 rounded-full mr-1"></span>
|
|
77
|
+
error
|
|
78
|
+
</span>
|
|
79
|
+
|
|
80
|
+
<span class="flex items-center">
|
|
81
|
+
<span class="w-2 h-2 bg-yellow-500 rounded-full mr-1"></span>
|
|
82
|
+
timeout
|
|
83
|
+
</span>
|
|
84
|
+
</div>
|
|
85
|
+
</div>
|
|
86
|
+
|
|
87
|
+
<%= link_to "View All", ruby_llm_agents.executions_path, data: { turbo: false }, class: "text-blue-600 hover:text-blue-700 text-sm font-medium" %>
|
|
88
|
+
</div>
|
|
89
|
+
</div>
|
|
90
|
+
|
|
91
|
+
<div
|
|
92
|
+
id="activity-feed"
|
|
93
|
+
class="px-6 py-2"
|
|
94
|
+
data-connection-target="activityFeed"
|
|
95
|
+
>
|
|
96
|
+
<% if @recent_executions.any? %>
|
|
97
|
+
<% @recent_executions.each do |execution| %>
|
|
98
|
+
<%= render partial: "rubyllm/agents/dashboard/execution_item", locals: { execution: execution } %>
|
|
99
|
+
<% end %>
|
|
100
|
+
<% else %>
|
|
101
|
+
<div id="empty-state" class="py-10 text-center text-gray-500">
|
|
102
|
+
<svg
|
|
103
|
+
class="w-12 h-12 mx-auto mb-3 text-gray-300"
|
|
104
|
+
fill="none"
|
|
105
|
+
stroke="currentColor"
|
|
106
|
+
viewBox="0 0 24 24"
|
|
107
|
+
>
|
|
108
|
+
<path
|
|
109
|
+
stroke-linecap="round"
|
|
110
|
+
stroke-linejoin="round"
|
|
111
|
+
stroke-width="1.5"
|
|
112
|
+
d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10"
|
|
113
|
+
/>
|
|
114
|
+
</svg>
|
|
115
|
+
|
|
116
|
+
<p>No executions yet. Create an agent and make a call!</p>
|
|
117
|
+
</div>
|
|
118
|
+
<% end %>
|
|
119
|
+
</div>
|
|
120
|
+
</div>
|
|
121
|
+
<% end %>
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
<%= link_to ruby_llm_agents.execution_path(execution), data: { turbo: false }, class: "block px-6 py-4 hover:bg-gray-50 transition-colors" do %>
|
|
2
|
+
<div class="flex items-center justify-between">
|
|
3
|
+
<div class="flex items-center space-x-4">
|
|
4
|
+
<!-- Status Badge -->
|
|
5
|
+
<span class="badge badge-<%= execution.status %>">
|
|
6
|
+
<%= execution.status %>
|
|
7
|
+
</span>
|
|
8
|
+
|
|
9
|
+
<!-- Agent Name -->
|
|
10
|
+
<div>
|
|
11
|
+
<p class="font-medium text-gray-900">
|
|
12
|
+
<%= execution.agent_type.gsub(/Agent$/, '') %>
|
|
13
|
+
</p>
|
|
14
|
+
|
|
15
|
+
<p class="text-sm text-gray-500">
|
|
16
|
+
v<%= execution.agent_version %> · <%= execution.model_id %>
|
|
17
|
+
</p>
|
|
18
|
+
</div>
|
|
19
|
+
</div>
|
|
20
|
+
|
|
21
|
+
<div class="flex items-center space-x-6 text-sm">
|
|
22
|
+
<!-- Tokens -->
|
|
23
|
+
<div class="text-right">
|
|
24
|
+
<p class="text-gray-900 font-medium">
|
|
25
|
+
<%= number_with_delimiter(execution.total_tokens || 0) %>
|
|
26
|
+
</p>
|
|
27
|
+
|
|
28
|
+
<p class="text-gray-500">tokens</p>
|
|
29
|
+
</div>
|
|
30
|
+
|
|
31
|
+
<!-- Cost -->
|
|
32
|
+
<div class="text-right">
|
|
33
|
+
<p class="text-gray-900 font-medium">
|
|
34
|
+
$<%= number_with_precision(execution.total_cost || 0, precision: 4) %>
|
|
35
|
+
</p>
|
|
36
|
+
|
|
37
|
+
<p class="text-gray-500">cost</p>
|
|
38
|
+
</div>
|
|
39
|
+
|
|
40
|
+
<!-- Duration -->
|
|
41
|
+
<div class="text-right">
|
|
42
|
+
<p class="text-gray-900 font-medium">
|
|
43
|
+
<%= number_with_delimiter(execution.duration_ms || 0) %>ms
|
|
44
|
+
</p>
|
|
45
|
+
|
|
46
|
+
<p class="text-gray-500">duration</p>
|
|
47
|
+
</div>
|
|
48
|
+
|
|
49
|
+
<!-- Time -->
|
|
50
|
+
<div class="text-right w-24">
|
|
51
|
+
<p class="text-gray-500">
|
|
52
|
+
<%= time_ago_in_words(execution.created_at) %> ago
|
|
53
|
+
</p>
|
|
54
|
+
</div>
|
|
55
|
+
</div>
|
|
56
|
+
</div>
|
|
57
|
+
|
|
58
|
+
<% if execution.status_error? && execution.error_message.present? %>
|
|
59
|
+
<div class="mt-2 text-sm text-red-600 bg-red-50 rounded px-3 py-2">
|
|
60
|
+
<strong><%= execution.error_class %>:</strong>
|
|
61
|
+
<%= truncate(execution.error_message, length: 100) %>
|
|
62
|
+
</div>
|
|
63
|
+
<% end %>
|
|
64
|
+
<% end %>
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
<%
|
|
2
|
+
has_filters = params[:agent_types].present? || params[:statuses].present? || params[:days].present?
|
|
3
|
+
selected_agents = params[:agent_types].present? ? (params[:agent_types].is_a?(Array) ? params[:agent_types] : params[:agent_types].split(",")) : []
|
|
4
|
+
selected_statuses = params[:statuses].present? ? (params[:statuses].is_a?(Array) ? params[:statuses] : params[:statuses].split(",")) : []
|
|
5
|
+
%>
|
|
6
|
+
<div class="bg-white rounded-xl border border-gray-200 p-4 mb-6">
|
|
7
|
+
<%= form_with url: ruby_llm_agents.executions_path, method: :get, data: { turbo_frame: "executions_content", turbo_action: "advance" }, id: "filters-form" do |f| %>
|
|
8
|
+
<div class="flex flex-wrap items-center gap-3">
|
|
9
|
+
<!-- Agent Type Filter (Multi-select) -->
|
|
10
|
+
<div class="relative filter-dropdown" data-filter="agent_types">
|
|
11
|
+
<button type="button" onclick="toggleDropdown(this)" class="flex items-center gap-2 px-3 py-2 text-sm bg-gray-50 border border-gray-200 rounded-lg hover:bg-gray-100 transition-colors <%= selected_agents.any? ? 'ring-2 ring-blue-500 ring-offset-1' : '' %>">
|
|
12
|
+
<svg class="w-4 h-4 text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
13
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"/>
|
|
14
|
+
</svg>
|
|
15
|
+
<span class="dropdown-label text-gray-700">
|
|
16
|
+
<% if selected_agents.empty? %>
|
|
17
|
+
All Agents
|
|
18
|
+
<% elsif selected_agents.length == 1 %>
|
|
19
|
+
<%= selected_agents.first.gsub(/Agent$/, '') %>
|
|
20
|
+
<% else %>
|
|
21
|
+
<%= selected_agents.length %> Agents
|
|
22
|
+
<% end %>
|
|
23
|
+
</span>
|
|
24
|
+
<svg class="w-4 h-4 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
25
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/>
|
|
26
|
+
</svg>
|
|
27
|
+
</button>
|
|
28
|
+
<div class="dropdown-menu hidden absolute z-10 mt-1 w-56 bg-white rounded-lg shadow-lg border border-gray-200 py-1">
|
|
29
|
+
<div class="px-3 py-2 border-b border-gray-100">
|
|
30
|
+
<label class="flex items-center gap-2 cursor-pointer">
|
|
31
|
+
<input type="checkbox" class="select-all-checkbox rounded border-gray-300 text-blue-600 focus:ring-blue-500" onchange="toggleAllOptions(this, 'agent_types')" <%= selected_agents.empty? ? 'checked' : '' %>>
|
|
32
|
+
<span class="text-sm font-medium text-gray-700">All Agents</span>
|
|
33
|
+
</label>
|
|
34
|
+
</div>
|
|
35
|
+
<% agent_types.each do |agent_type| %>
|
|
36
|
+
<label class="flex items-center gap-2 px-3 py-2 text-sm text-gray-700 hover:bg-gray-50 cursor-pointer">
|
|
37
|
+
<input type="checkbox" name="agent_types[]" value="<%= agent_type %>" class="filter-checkbox rounded border-gray-300 text-blue-600 focus:ring-blue-500" onchange="updateMultiSelect('agent_types')" <%= selected_agents.include?(agent_type) ? 'checked' : '' %>>
|
|
38
|
+
<svg class="w-4 h-4 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
39
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"/>
|
|
40
|
+
</svg>
|
|
41
|
+
<%= agent_type.gsub(/Agent$/, '') %>
|
|
42
|
+
</label>
|
|
43
|
+
<% end %>
|
|
44
|
+
</div>
|
|
45
|
+
</div>
|
|
46
|
+
|
|
47
|
+
<!-- Status Filter (Multi-select) -->
|
|
48
|
+
<div class="relative filter-dropdown" data-filter="statuses">
|
|
49
|
+
<button type="button" onclick="toggleDropdown(this)" class="flex items-center gap-2 px-3 py-2 text-sm bg-gray-50 border border-gray-200 rounded-lg hover:bg-gray-100 transition-colors <%= selected_statuses.any? ? 'ring-2 ring-blue-500 ring-offset-1' : '' %>">
|
|
50
|
+
<% if selected_statuses.length == 1
|
|
51
|
+
status_color = case selected_statuses.first
|
|
52
|
+
when 'success' then 'bg-green-500'
|
|
53
|
+
when 'error' then 'bg-red-500'
|
|
54
|
+
when 'running' then 'bg-blue-500'
|
|
55
|
+
when 'timeout' then 'bg-yellow-500'
|
|
56
|
+
else 'bg-gray-400'
|
|
57
|
+
end
|
|
58
|
+
else
|
|
59
|
+
status_color = 'bg-gray-400'
|
|
60
|
+
end %>
|
|
61
|
+
<span class="w-2 h-2 rounded-full <%= status_color %>"></span>
|
|
62
|
+
<span class="dropdown-label text-gray-700">
|
|
63
|
+
<% if selected_statuses.empty? %>
|
|
64
|
+
All Statuses
|
|
65
|
+
<% elsif selected_statuses.length == 1 %>
|
|
66
|
+
<%= selected_statuses.first.capitalize %>
|
|
67
|
+
<% else %>
|
|
68
|
+
<%= selected_statuses.length %> Statuses
|
|
69
|
+
<% end %>
|
|
70
|
+
</span>
|
|
71
|
+
<svg class="w-4 h-4 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
72
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/>
|
|
73
|
+
</svg>
|
|
74
|
+
</button>
|
|
75
|
+
<div class="dropdown-menu hidden absolute z-10 mt-1 w-44 bg-white rounded-lg shadow-lg border border-gray-200 py-1">
|
|
76
|
+
<div class="px-3 py-2 border-b border-gray-100">
|
|
77
|
+
<label class="flex items-center gap-2 cursor-pointer">
|
|
78
|
+
<input type="checkbox" class="select-all-checkbox rounded border-gray-300 text-blue-600 focus:ring-blue-500" onchange="toggleAllOptions(this, 'statuses')" <%= selected_statuses.empty? ? 'checked' : '' %>>
|
|
79
|
+
<span class="text-sm font-medium text-gray-700">All Statuses</span>
|
|
80
|
+
</label>
|
|
81
|
+
</div>
|
|
82
|
+
<label class="flex items-center gap-2 px-3 py-2 text-sm text-gray-700 hover:bg-gray-50 cursor-pointer">
|
|
83
|
+
<input type="checkbox" name="statuses[]" value="success" class="filter-checkbox rounded border-gray-300 text-blue-600 focus:ring-blue-500" onchange="updateMultiSelect('statuses')" <%= selected_statuses.include?('success') ? 'checked' : '' %>>
|
|
84
|
+
<span class="w-2 h-2 rounded-full bg-green-500"></span>
|
|
85
|
+
Success
|
|
86
|
+
</label>
|
|
87
|
+
<label class="flex items-center gap-2 px-3 py-2 text-sm text-gray-700 hover:bg-gray-50 cursor-pointer">
|
|
88
|
+
<input type="checkbox" name="statuses[]" value="error" class="filter-checkbox rounded border-gray-300 text-blue-600 focus:ring-blue-500" onchange="updateMultiSelect('statuses')" <%= selected_statuses.include?('error') ? 'checked' : '' %>>
|
|
89
|
+
<span class="w-2 h-2 rounded-full bg-red-500"></span>
|
|
90
|
+
Error
|
|
91
|
+
</label>
|
|
92
|
+
<label class="flex items-center gap-2 px-3 py-2 text-sm text-gray-700 hover:bg-gray-50 cursor-pointer">
|
|
93
|
+
<input type="checkbox" name="statuses[]" value="running" class="filter-checkbox rounded border-gray-300 text-blue-600 focus:ring-blue-500" onchange="updateMultiSelect('statuses')" <%= selected_statuses.include?('running') ? 'checked' : '' %>>
|
|
94
|
+
<span class="w-2 h-2 rounded-full bg-blue-500 animate-pulse"></span>
|
|
95
|
+
Running
|
|
96
|
+
</label>
|
|
97
|
+
<label class="flex items-center gap-2 px-3 py-2 text-sm text-gray-700 hover:bg-gray-50 cursor-pointer">
|
|
98
|
+
<input type="checkbox" name="statuses[]" value="timeout" class="filter-checkbox rounded border-gray-300 text-blue-600 focus:ring-blue-500" onchange="updateMultiSelect('statuses')" <%= selected_statuses.include?('timeout') ? 'checked' : '' %>>
|
|
99
|
+
<span class="w-2 h-2 rounded-full bg-yellow-500"></span>
|
|
100
|
+
Timeout
|
|
101
|
+
</label>
|
|
102
|
+
</div>
|
|
103
|
+
</div>
|
|
104
|
+
|
|
105
|
+
<!-- Time Range Filter -->
|
|
106
|
+
<div class="relative filter-dropdown" data-filter="days">
|
|
107
|
+
<button type="button" onclick="toggleDropdown(this)" class="flex items-center gap-2 px-3 py-2 text-sm bg-gray-50 border border-gray-200 rounded-lg hover:bg-gray-100 transition-colors <%= params[:days].present? ? 'ring-2 ring-blue-500 ring-offset-1' : '' %>">
|
|
108
|
+
<svg class="w-4 h-4 text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
109
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"/>
|
|
110
|
+
</svg>
|
|
111
|
+
<span class="dropdown-label text-gray-700">
|
|
112
|
+
<% case params[:days]
|
|
113
|
+
when '1' then %>Today<%
|
|
114
|
+
when '7' then %>Last 7 Days<%
|
|
115
|
+
when '30' then %>Last 30 Days<%
|
|
116
|
+
else %>All Time<%
|
|
117
|
+
end %>
|
|
118
|
+
</span>
|
|
119
|
+
<svg class="w-4 h-4 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
120
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/>
|
|
121
|
+
</svg>
|
|
122
|
+
</button>
|
|
123
|
+
<div class="dropdown-menu hidden absolute z-10 mt-1 w-40 bg-white rounded-lg shadow-lg border border-gray-200 py-1">
|
|
124
|
+
<a href="#" onclick="selectSingleFilter('days', '', 'All Time'); return false;" class="flex items-center gap-2 px-3 py-2 text-sm text-gray-700 hover:bg-gray-50 <%= params[:days].blank? ? 'bg-blue-50 text-blue-700' : '' %>">
|
|
125
|
+
<svg class="w-4 h-4 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
126
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"/>
|
|
127
|
+
</svg>
|
|
128
|
+
All Time
|
|
129
|
+
</a>
|
|
130
|
+
<a href="#" onclick="selectSingleFilter('days', '1', 'Today'); return false;" class="flex items-center gap-2 px-3 py-2 text-sm text-gray-700 hover:bg-gray-50 <%= params[:days] == '1' ? 'bg-blue-50 text-blue-700' : '' %>">
|
|
131
|
+
<svg class="w-4 h-4 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
132
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
|
133
|
+
</svg>
|
|
134
|
+
Today
|
|
135
|
+
</a>
|
|
136
|
+
<a href="#" onclick="selectSingleFilter('days', '7', 'Last 7 Days'); return false;" class="flex items-center gap-2 px-3 py-2 text-sm text-gray-700 hover:bg-gray-50 <%= params[:days] == '7' ? 'bg-blue-50 text-blue-700' : '' %>">
|
|
137
|
+
<svg class="w-4 h-4 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
138
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"/>
|
|
139
|
+
</svg>
|
|
140
|
+
Last 7 Days
|
|
141
|
+
</a>
|
|
142
|
+
<a href="#" onclick="selectSingleFilter('days', '30', 'Last 30 Days'); return false;" class="flex items-center gap-2 px-3 py-2 text-sm text-gray-700 hover:bg-gray-50 <%= params[:days] == '30' ? 'bg-blue-50 text-blue-700' : '' %>">
|
|
143
|
+
<svg class="w-4 h-4 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
144
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"/>
|
|
145
|
+
</svg>
|
|
146
|
+
Last 30 Days
|
|
147
|
+
</a>
|
|
148
|
+
</div>
|
|
149
|
+
<%= f.hidden_field :days, value: params[:days], id: "filter_days" %>
|
|
150
|
+
</div>
|
|
151
|
+
|
|
152
|
+
<!-- Clear Filters -->
|
|
153
|
+
<% if has_filters %>
|
|
154
|
+
<%= link_to ruby_llm_agents.executions_path, data: { turbo_frame: "executions_content", turbo_action: "advance" }, class: "flex items-center gap-1 px-3 py-2 text-sm text-red-500 hover:text-red-600 hover:bg-red-50 rounded-lg transition-colors" do %>
|
|
155
|
+
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
156
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
|
|
157
|
+
</svg>
|
|
158
|
+
Clear
|
|
159
|
+
<% end %>
|
|
160
|
+
<% end %>
|
|
161
|
+
|
|
162
|
+
<!-- Stats Summary (right aligned) -->
|
|
163
|
+
<div class="ml-auto flex items-center gap-4 text-sm text-gray-500">
|
|
164
|
+
<span><%= number_to_human_short(filter_stats[:total_count]) %> executions</span>
|
|
165
|
+
<span class="text-gray-300">|</span>
|
|
166
|
+
<span><%= number_to_human_short(filter_stats[:total_cost], prefix: "$", precision: 2) %></span>
|
|
167
|
+
<span class="text-gray-300">|</span>
|
|
168
|
+
<span><%= number_to_human_short(filter_stats[:total_tokens]) %> tokens</span>
|
|
169
|
+
</div>
|
|
170
|
+
</div>
|
|
171
|
+
<% end %>
|
|
172
|
+
</div>
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
<div class="bg-white rounded-lg shadow overflow-hidden">
|
|
2
|
+
<% if executions.empty? %>
|
|
3
|
+
<div class="px-6 py-12 text-center text-gray-500">
|
|
4
|
+
<p class="text-lg">No executions found</p>
|
|
5
|
+
<p class="text-sm mt-1">Try adjusting your filters</p>
|
|
6
|
+
</div>
|
|
7
|
+
<% else %>
|
|
8
|
+
<div class="overflow-x-auto">
|
|
9
|
+
<table class="min-w-full divide-y divide-gray-200">
|
|
10
|
+
<thead class="bg-gray-50">
|
|
11
|
+
<tr>
|
|
12
|
+
<th
|
|
13
|
+
scope="col"
|
|
14
|
+
class="
|
|
15
|
+
px-4 py-3 text-left text-xs font-medium text-gray-500
|
|
16
|
+
uppercase tracking-wider
|
|
17
|
+
"
|
|
18
|
+
>
|
|
19
|
+
Agent
|
|
20
|
+
</th>
|
|
21
|
+
|
|
22
|
+
<th
|
|
23
|
+
scope="col"
|
|
24
|
+
class="
|
|
25
|
+
px-4 py-3 text-left text-xs font-medium text-gray-500
|
|
26
|
+
uppercase tracking-wider
|
|
27
|
+
"
|
|
28
|
+
>
|
|
29
|
+
Status
|
|
30
|
+
</th>
|
|
31
|
+
|
|
32
|
+
<th
|
|
33
|
+
scope="col"
|
|
34
|
+
class="
|
|
35
|
+
px-4 py-3 text-left text-xs font-medium text-gray-500
|
|
36
|
+
uppercase tracking-wider
|
|
37
|
+
"
|
|
38
|
+
>
|
|
39
|
+
Version
|
|
40
|
+
</th>
|
|
41
|
+
|
|
42
|
+
<th
|
|
43
|
+
scope="col"
|
|
44
|
+
class="
|
|
45
|
+
px-4 py-3 text-right text-xs font-medium text-gray-500
|
|
46
|
+
uppercase tracking-wider
|
|
47
|
+
"
|
|
48
|
+
>
|
|
49
|
+
Tokens
|
|
50
|
+
</th>
|
|
51
|
+
|
|
52
|
+
<th
|
|
53
|
+
scope="col"
|
|
54
|
+
class="
|
|
55
|
+
px-4 py-3 text-right text-xs font-medium text-gray-500
|
|
56
|
+
uppercase tracking-wider
|
|
57
|
+
"
|
|
58
|
+
>
|
|
59
|
+
Cost
|
|
60
|
+
</th>
|
|
61
|
+
|
|
62
|
+
<th
|
|
63
|
+
scope="col"
|
|
64
|
+
class="
|
|
65
|
+
px-4 py-3 text-right text-xs font-medium text-gray-500
|
|
66
|
+
uppercase tracking-wider
|
|
67
|
+
"
|
|
68
|
+
>
|
|
69
|
+
Duration
|
|
70
|
+
</th>
|
|
71
|
+
|
|
72
|
+
<th
|
|
73
|
+
scope="col"
|
|
74
|
+
class="
|
|
75
|
+
px-4 py-3 text-right text-xs font-medium text-gray-500
|
|
76
|
+
uppercase tracking-wider
|
|
77
|
+
"
|
|
78
|
+
>
|
|
79
|
+
Time
|
|
80
|
+
</th>
|
|
81
|
+
</tr>
|
|
82
|
+
</thead>
|
|
83
|
+
|
|
84
|
+
<tbody class="bg-white divide-y divide-gray-100">
|
|
85
|
+
<% executions.each do |execution| %>
|
|
86
|
+
<tr
|
|
87
|
+
class="hover:bg-gray-50 transition-colors cursor-pointer"
|
|
88
|
+
onclick="window.location='<%= ruby_llm_agents.execution_path(execution) %>'"
|
|
89
|
+
>
|
|
90
|
+
<td class="px-4 py-3 whitespace-nowrap">
|
|
91
|
+
<span class="text-sm font-medium text-gray-900">
|
|
92
|
+
<%= execution.agent_type.gsub(/Agent$/, '') %>
|
|
93
|
+
</span>
|
|
94
|
+
</td>
|
|
95
|
+
|
|
96
|
+
<td class="px-4 py-3 whitespace-nowrap">
|
|
97
|
+
<%= render "rubyllm/agents/shared/status_badge", status: execution.status %>
|
|
98
|
+
</td>
|
|
99
|
+
|
|
100
|
+
<td class="px-4 py-3 whitespace-nowrap text-sm text-gray-500">
|
|
101
|
+
v<%= execution.agent_version %>
|
|
102
|
+
</td>
|
|
103
|
+
|
|
104
|
+
<td
|
|
105
|
+
class="
|
|
106
|
+
px-4 py-3 whitespace-nowrap text-sm text-gray-900
|
|
107
|
+
text-right font-medium
|
|
108
|
+
"
|
|
109
|
+
>
|
|
110
|
+
<%= number_to_human_short(execution.total_tokens || 0) %>
|
|
111
|
+
</td>
|
|
112
|
+
|
|
113
|
+
<td
|
|
114
|
+
class="
|
|
115
|
+
px-4 py-3 whitespace-nowrap text-sm text-gray-900
|
|
116
|
+
text-right font-medium
|
|
117
|
+
"
|
|
118
|
+
>
|
|
119
|
+
<%= number_to_human_short(execution.total_cost || 0, prefix: "$", precision: 2) %>
|
|
120
|
+
</td>
|
|
121
|
+
|
|
122
|
+
<td
|
|
123
|
+
class="
|
|
124
|
+
px-4 py-3 whitespace-nowrap text-sm text-gray-900
|
|
125
|
+
text-right font-medium
|
|
126
|
+
"
|
|
127
|
+
>
|
|
128
|
+
<%= number_with_delimiter(execution.duration_ms || 0) %>ms
|
|
129
|
+
</td>
|
|
130
|
+
|
|
131
|
+
<td class="px-4 py-3 whitespace-nowrap text-sm text-gray-500 text-right">
|
|
132
|
+
<%= time_ago_in_words(execution.created_at) %> ago
|
|
133
|
+
</td>
|
|
134
|
+
</tr>
|
|
135
|
+
|
|
136
|
+
<% if execution.status_error? && execution.error_message.present? %>
|
|
137
|
+
<tr class="bg-red-50">
|
|
138
|
+
<td colspan="7" class="px-4 py-2">
|
|
139
|
+
<p class="text-xs text-red-600">
|
|
140
|
+
<span class="font-medium">
|
|
141
|
+
<%= execution.error_class %>:
|
|
142
|
+
</span>
|
|
143
|
+
|
|
144
|
+
<%= truncate(execution.error_message, length: 150) %>
|
|
145
|
+
</p>
|
|
146
|
+
</td>
|
|
147
|
+
</tr>
|
|
148
|
+
<% end %>
|
|
149
|
+
<% end %>
|
|
150
|
+
</tbody>
|
|
151
|
+
</table>
|
|
152
|
+
</div>
|
|
153
|
+
<% if pagination && pagination[:total_pages] > 1 %>
|
|
154
|
+
<% current_page = pagination[:current_page]
|
|
155
|
+
total_pages = pagination[:total_pages]
|
|
156
|
+
total_count = pagination[:total_count]
|
|
157
|
+
per_page = pagination[:per_page]
|
|
158
|
+
|
|
159
|
+
from_record = ((current_page - 1) * per_page) + 1
|
|
160
|
+
to_record = [current_page * per_page, total_count].min %>
|
|
161
|
+
<div class="px-4 py-4 flex items-center justify-between border-t border-gray-100">
|
|
162
|
+
<p class="text-sm text-gray-500">
|
|
163
|
+
Showing <%= from_record %>-<%= to_record %> of
|
|
164
|
+
<%= number_with_delimiter(total_count) %> executions
|
|
165
|
+
</p>
|
|
166
|
+
|
|
167
|
+
<nav class="flex items-center space-x-1">
|
|
168
|
+
<% if current_page > 1 %>
|
|
169
|
+
<%= link_to "Previous", url_for(page: current_page - 1), data: { turbo_frame: "executions_content", turbo_action: "advance" }, class: "px-3 py-1.5 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md hover:bg-gray-50" %>
|
|
170
|
+
<% else %>
|
|
171
|
+
<span
|
|
172
|
+
class="
|
|
173
|
+
px-3 py-1.5 text-sm font-medium text-gray-400 bg-gray-100
|
|
174
|
+
border border-gray-200 rounded-md cursor-not-allowed
|
|
175
|
+
"
|
|
176
|
+
>
|
|
177
|
+
Previous
|
|
178
|
+
</span>
|
|
179
|
+
<% end %>
|
|
180
|
+
|
|
181
|
+
<% window = 2
|
|
182
|
+
left_edge = 1
|
|
183
|
+
right_edge = 1
|
|
184
|
+
|
|
185
|
+
pages_to_show = []
|
|
186
|
+
(1..total_pages).each do |page|
|
|
187
|
+
if page <= left_edge ||
|
|
188
|
+
page > total_pages - right_edge ||
|
|
189
|
+
(page >= current_page - window && page <= current_page + window)
|
|
190
|
+
pages_to_show << page
|
|
191
|
+
elsif pages_to_show.last != :gap
|
|
192
|
+
pages_to_show << :gap
|
|
193
|
+
end
|
|
194
|
+
end %>
|
|
195
|
+
|
|
196
|
+
<% pages_to_show.each do |page| %>
|
|
197
|
+
<% if page == :gap %>
|
|
198
|
+
<span class="px-2 py-1.5 text-sm text-gray-500">...</span>
|
|
199
|
+
<% elsif page == current_page %>
|
|
200
|
+
<span
|
|
201
|
+
class="
|
|
202
|
+
px-3 py-1.5 text-sm font-medium text-white bg-blue-600
|
|
203
|
+
border border-blue-600 rounded-md
|
|
204
|
+
"
|
|
205
|
+
>
|
|
206
|
+
<%= page %>
|
|
207
|
+
</span>
|
|
208
|
+
<% else %>
|
|
209
|
+
<%= link_to page, url_for(page: page), data: { turbo_frame: "executions_content", turbo_action: "advance" }, class: "px-3 py-1.5 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md hover:bg-gray-50" %>
|
|
210
|
+
<% end %>
|
|
211
|
+
<% end %>
|
|
212
|
+
|
|
213
|
+
<% if current_page < total_pages %>
|
|
214
|
+
<%= link_to "Next", url_for(page: current_page + 1), data: { turbo_frame: "executions_content", turbo_action: "advance" }, class: "px-3 py-1.5 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md hover:bg-gray-50" %>
|
|
215
|
+
<% else %>
|
|
216
|
+
<span
|
|
217
|
+
class="
|
|
218
|
+
px-3 py-1.5 text-sm font-medium text-gray-400 bg-gray-100
|
|
219
|
+
border border-gray-200 rounded-md cursor-not-allowed
|
|
220
|
+
"
|
|
221
|
+
>
|
|
222
|
+
Next
|
|
223
|
+
</span>
|
|
224
|
+
<% end %>
|
|
225
|
+
</nav>
|
|
226
|
+
</div>
|
|
227
|
+
<% end %>
|
|
228
|
+
<% end %>
|
|
229
|
+
</div>
|