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,83 @@
|
|
|
1
|
+
<h2 class="text-2xl font-bold text-gray-900 mb-6">Executions</h2>
|
|
2
|
+
|
|
3
|
+
<%= turbo_frame_tag "executions_content" do %>
|
|
4
|
+
<%= render partial: "rubyllm/agents/executions/filters", locals: { agent_types: @agent_types, filter_stats: @filter_stats } %>
|
|
5
|
+
<%= render partial: "rubyllm/agents/executions/list", locals: { executions: @executions, pagination: @pagination, filter_stats: @filter_stats } %>
|
|
6
|
+
<% end %>
|
|
7
|
+
|
|
8
|
+
<script>
|
|
9
|
+
function toggleDropdown(button) {
|
|
10
|
+
document.querySelectorAll('.filter-dropdown .dropdown-menu').forEach(menu => {
|
|
11
|
+
if (menu !== button.nextElementSibling) {
|
|
12
|
+
menu.classList.add('hidden');
|
|
13
|
+
}
|
|
14
|
+
});
|
|
15
|
+
button.nextElementSibling.classList.toggle('hidden');
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function selectSingleFilter(name, value, label) {
|
|
19
|
+
document.getElementById('filter_' + name).value = value;
|
|
20
|
+
// Update button label
|
|
21
|
+
const dropdown = document.querySelector(`[data-filter="${name}"]`);
|
|
22
|
+
dropdown.querySelector('.dropdown-label').textContent = label;
|
|
23
|
+
// Update ring
|
|
24
|
+
const button = dropdown.querySelector('button');
|
|
25
|
+
if (value) {
|
|
26
|
+
button.classList.add('ring-2', 'ring-blue-500', 'ring-offset-1');
|
|
27
|
+
} else {
|
|
28
|
+
button.classList.remove('ring-2', 'ring-blue-500', 'ring-offset-1');
|
|
29
|
+
}
|
|
30
|
+
// Close dropdown and submit
|
|
31
|
+
dropdown.querySelector('.dropdown-menu').classList.add('hidden');
|
|
32
|
+
document.getElementById('filters-form').requestSubmit();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function toggleAllOptions(checkbox, filterName) {
|
|
36
|
+
const dropdown = document.querySelector(`[data-filter="${filterName}"]`);
|
|
37
|
+
const checkboxes = dropdown.querySelectorAll('.filter-checkbox');
|
|
38
|
+
|
|
39
|
+
if (checkbox.checked) {
|
|
40
|
+
// Uncheck all specific options
|
|
41
|
+
checkboxes.forEach(cb => cb.checked = false);
|
|
42
|
+
updateMultiSelect(filterName);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function updateMultiSelect(filterName) {
|
|
47
|
+
const dropdown = document.querySelector(`[data-filter="${filterName}"]`);
|
|
48
|
+
const checkboxes = dropdown.querySelectorAll('.filter-checkbox:checked');
|
|
49
|
+
const selectAllCheckbox = dropdown.querySelector('.select-all-checkbox');
|
|
50
|
+
const button = dropdown.querySelector('button');
|
|
51
|
+
const label = dropdown.querySelector('.dropdown-label');
|
|
52
|
+
|
|
53
|
+
const count = checkboxes.length;
|
|
54
|
+
|
|
55
|
+
// Update "All" checkbox
|
|
56
|
+
selectAllCheckbox.checked = (count === 0);
|
|
57
|
+
|
|
58
|
+
// Update label
|
|
59
|
+
if (count === 0) {
|
|
60
|
+
label.textContent = filterName === 'agent_types' ? 'All Agents' : 'All Statuses';
|
|
61
|
+
button.classList.remove('ring-2', 'ring-blue-500', 'ring-offset-1');
|
|
62
|
+
} else if (count === 1) {
|
|
63
|
+
const value = checkboxes[0].value;
|
|
64
|
+
label.textContent = filterName === 'agent_types' ? value.replace(/Agent$/, '') : value.charAt(0).toUpperCase() + value.slice(1);
|
|
65
|
+
button.classList.add('ring-2', 'ring-blue-500', 'ring-offset-1');
|
|
66
|
+
} else {
|
|
67
|
+
label.textContent = `${count} ${filterName === 'agent_types' ? 'Agents' : 'Statuses'}`;
|
|
68
|
+
button.classList.add('ring-2', 'ring-blue-500', 'ring-offset-1');
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Submit form
|
|
72
|
+
document.getElementById('filters-form').requestSubmit();
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Close dropdowns when clicking outside
|
|
76
|
+
document.addEventListener('click', function(e) {
|
|
77
|
+
if (!e.target.closest('.filter-dropdown')) {
|
|
78
|
+
document.querySelectorAll('.filter-dropdown .dropdown-menu').forEach(menu => {
|
|
79
|
+
menu.classList.add('hidden');
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
</script>
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
<%= turbo_stream.replace "executions_content" do %>
|
|
2
|
+
<%= render partial: "rubyllm/agents/executions/filters", locals: { agent_types: @agent_types, filter_stats: @filter_stats } %>
|
|
3
|
+
<%= render partial: "rubyllm/agents/executions/list", locals: { executions: @executions, pagination: @pagination, filter_stats: @filter_stats } %>
|
|
4
|
+
<% end %>
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
<div class="mb-6">
|
|
2
|
+
<%= link_to ruby_llm_agents.executions_path, class: "inline-flex items-center text-sm text-gray-500 hover:text-gray-700" do %>
|
|
3
|
+
<svg
|
|
4
|
+
class="w-4 h-4 mr-1"
|
|
5
|
+
fill="none"
|
|
6
|
+
stroke="currentColor"
|
|
7
|
+
viewBox="0 0 24 24"
|
|
8
|
+
>
|
|
9
|
+
<path
|
|
10
|
+
stroke-linecap="round"
|
|
11
|
+
stroke-linejoin="round"
|
|
12
|
+
stroke-width="2"
|
|
13
|
+
d="M10 19l-7-7m0 0l7-7m-7 7h18"
|
|
14
|
+
/>
|
|
15
|
+
</svg>
|
|
16
|
+
Back to Executions
|
|
17
|
+
<% end %>
|
|
18
|
+
</div>
|
|
19
|
+
|
|
20
|
+
<!-- Header -->
|
|
21
|
+
<div class="bg-white border border-gray-200 rounded-xl p-6 mb-6">
|
|
22
|
+
<div class="flex items-center justify-between">
|
|
23
|
+
<div>
|
|
24
|
+
<div class="flex items-center gap-3">
|
|
25
|
+
<h2 class="text-xl font-bold text-gray-900">
|
|
26
|
+
<%= @execution.agent_type.gsub(/Agent$/, '') %>
|
|
27
|
+
</h2>
|
|
28
|
+
|
|
29
|
+
<%= render "rubyllm/agents/shared/status_badge", status: @execution.status, size: :md %>
|
|
30
|
+
</div>
|
|
31
|
+
|
|
32
|
+
<p class="text-sm text-gray-500 mt-1">
|
|
33
|
+
Execution #<%= @execution.id %> · v<%= @execution.agent_version %>
|
|
34
|
+
</p>
|
|
35
|
+
</div>
|
|
36
|
+
|
|
37
|
+
<div class="text-right text-sm text-gray-500">
|
|
38
|
+
<p><%= @execution.created_at.strftime("%b %d, %Y at %H:%M") %></p>
|
|
39
|
+
|
|
40
|
+
<p class="text-xs text-gray-400">
|
|
41
|
+
<%= time_ago_in_words(@execution.created_at) %> ago
|
|
42
|
+
</p>
|
|
43
|
+
</div>
|
|
44
|
+
</div>
|
|
45
|
+
</div>
|
|
46
|
+
|
|
47
|
+
<!-- Stats Grid -->
|
|
48
|
+
<div class="grid grid-cols-2 md:grid-cols-4 gap-4 mb-6">
|
|
49
|
+
<%= render "rubyllm/agents/shared/stat_card",
|
|
50
|
+
title: "Model",
|
|
51
|
+
value: @execution.model_id,
|
|
52
|
+
icon: "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",
|
|
53
|
+
icon_color: "text-blue-500" %>
|
|
54
|
+
|
|
55
|
+
<%= render "rubyllm/agents/shared/stat_card",
|
|
56
|
+
title: "Duration",
|
|
57
|
+
value: "#{number_to_human_short(@execution.duration_ms || 0)} ms",
|
|
58
|
+
icon: "M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z",
|
|
59
|
+
icon_color: "text-purple-500" %>
|
|
60
|
+
|
|
61
|
+
<%= render "rubyllm/agents/shared/stat_card",
|
|
62
|
+
title: "Total Tokens",
|
|
63
|
+
value: number_to_human_short(@execution.total_tokens || 0),
|
|
64
|
+
icon: "M7 20l4-16m2 16l4-16M6 9h14M4 15h14",
|
|
65
|
+
icon_color: "text-indigo-500" %>
|
|
66
|
+
|
|
67
|
+
<%= render "rubyllm/agents/shared/stat_card",
|
|
68
|
+
title: "Total Cost",
|
|
69
|
+
value: number_to_human_short(@execution.total_cost || 0, prefix: "$", precision: 2),
|
|
70
|
+
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",
|
|
71
|
+
icon_color: "text-amber-500" %>
|
|
72
|
+
</div>
|
|
73
|
+
|
|
74
|
+
<!-- Token Breakdown -->
|
|
75
|
+
<div class="bg-white border border-gray-200 rounded-xl p-6 mb-6">
|
|
76
|
+
<h3 class="text-sm font-medium text-gray-500 uppercase tracking-wide mb-4">Token Usage</h3>
|
|
77
|
+
|
|
78
|
+
<!-- Token Distribution Bar -->
|
|
79
|
+
<%
|
|
80
|
+
input_tokens = @execution.input_tokens || 0
|
|
81
|
+
output_tokens = @execution.output_tokens || 0
|
|
82
|
+
total = input_tokens + output_tokens
|
|
83
|
+
input_pct = total > 0 ? (input_tokens.to_f / total * 100).round(1) : 0
|
|
84
|
+
output_pct = total > 0 ? (output_tokens.to_f / total * 100).round(1) : 0
|
|
85
|
+
%>
|
|
86
|
+
<div class="mb-6">
|
|
87
|
+
<div class="flex justify-between text-xs mb-1.5">
|
|
88
|
+
<span class="text-blue-600 font-medium">Input: <%= number_to_human_short(input_tokens) %> (<%= input_pct %>%)</span>
|
|
89
|
+
<span class="text-green-600 font-medium">Output: <%= number_to_human_short(output_tokens) %> (<%= output_pct %>%)</span>
|
|
90
|
+
</div>
|
|
91
|
+
<div class="h-2.5 bg-gray-100 rounded-full overflow-hidden flex">
|
|
92
|
+
<div class="bg-blue-500 transition-all" style="width: <%= input_pct %>%"></div>
|
|
93
|
+
<div class="bg-green-500 transition-all" style="width: <%= output_pct %>%"></div>
|
|
94
|
+
</div>
|
|
95
|
+
</div>
|
|
96
|
+
|
|
97
|
+
<!-- Detailed Metrics -->
|
|
98
|
+
<div class="grid grid-cols-2 md:grid-cols-5 gap-6 pt-4 border-t border-gray-100">
|
|
99
|
+
<div>
|
|
100
|
+
<p class="text-xs text-gray-500 uppercase tracking-wide">Input</p>
|
|
101
|
+
<p class="text-lg font-semibold text-gray-900"><%= number_to_human_short(@execution.input_tokens || 0) %></p>
|
|
102
|
+
<p class="text-xs text-gray-400"><%= number_to_human_short(@execution.input_cost || 0, prefix: "$", precision: 4) %></p>
|
|
103
|
+
</div>
|
|
104
|
+
|
|
105
|
+
<div>
|
|
106
|
+
<p class="text-xs text-gray-500 uppercase tracking-wide">Output</p>
|
|
107
|
+
<p class="text-lg font-semibold text-gray-900"><%= number_to_human_short(@execution.output_tokens || 0) %></p>
|
|
108
|
+
<p class="text-xs text-gray-400"><%= number_to_human_short(@execution.output_cost || 0, prefix: "$", precision: 4) %></p>
|
|
109
|
+
</div>
|
|
110
|
+
|
|
111
|
+
<div>
|
|
112
|
+
<p class="text-xs text-gray-500 uppercase tracking-wide">Cached</p>
|
|
113
|
+
<p class="text-lg font-semibold text-gray-900"><%= number_to_human_short(@execution.cached_tokens || 0) %></p>
|
|
114
|
+
</div>
|
|
115
|
+
|
|
116
|
+
<div>
|
|
117
|
+
<p class="text-xs text-gray-500 uppercase tracking-wide">Cache Creation</p>
|
|
118
|
+
<p class="text-lg font-semibold text-gray-900"><%= number_to_human_short(@execution.cache_creation_tokens || 0) %></p>
|
|
119
|
+
</div>
|
|
120
|
+
|
|
121
|
+
<div>
|
|
122
|
+
<p class="text-xs text-gray-500 uppercase tracking-wide">Tokens/Sec</p>
|
|
123
|
+
<p class="text-lg font-semibold text-gray-900"><%= @execution.tokens_per_second || 'N/A' %></p>
|
|
124
|
+
</div>
|
|
125
|
+
</div>
|
|
126
|
+
</div>
|
|
127
|
+
|
|
128
|
+
<% if @execution.status_error? %>
|
|
129
|
+
<!-- Error Details -->
|
|
130
|
+
<div class="bg-red-50 border border-red-200 rounded-lg p-6 mb-6">
|
|
131
|
+
<h3 class="text-lg font-semibold text-red-800 mb-2">Error Details</h3>
|
|
132
|
+
|
|
133
|
+
<p class="font-mono text-sm text-red-700 mb-2">
|
|
134
|
+
<%= @execution.error_class %>
|
|
135
|
+
</p>
|
|
136
|
+
|
|
137
|
+
<pre class="bg-red-100 rounded p-4 text-sm text-red-900 overflow-x-auto"><%= @execution.error_message %></pre>
|
|
138
|
+
</div>
|
|
139
|
+
<% end %>
|
|
140
|
+
|
|
141
|
+
<!-- Parameters -->
|
|
142
|
+
<div class="bg-white border border-gray-200 rounded-xl p-6 mb-6">
|
|
143
|
+
<div class="flex items-center justify-between mb-4">
|
|
144
|
+
<h3 class="text-sm font-medium text-gray-500 uppercase tracking-wide">Parameters</h3>
|
|
145
|
+
<button
|
|
146
|
+
type="button"
|
|
147
|
+
data-copy-json="<%= Base64.strict_encode64(JSON.pretty_generate(@execution.parameters || {})) %>"
|
|
148
|
+
class="copy-json-btn inline-flex items-center gap-1.5 px-2.5 py-1.5 text-xs font-medium text-gray-500 hover:text-gray-700 hover:bg-gray-100 rounded-md transition-colors"
|
|
149
|
+
>
|
|
150
|
+
<svg class="w-4 h-4 copy-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
151
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"/>
|
|
152
|
+
</svg>
|
|
153
|
+
<svg class="w-4 h-4 check-icon hidden" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
154
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/>
|
|
155
|
+
</svg>
|
|
156
|
+
<span>Copy</span>
|
|
157
|
+
</button>
|
|
158
|
+
</div>
|
|
159
|
+
<pre class="bg-gray-50 rounded-lg p-4 text-sm overflow-x-auto font-mono"><%= highlight_json(@execution.parameters || {}) %></pre>
|
|
160
|
+
</div>
|
|
161
|
+
|
|
162
|
+
<!-- Response -->
|
|
163
|
+
<% if @execution.response.present? %>
|
|
164
|
+
<div class="bg-white border border-gray-200 rounded-xl p-6 mb-6">
|
|
165
|
+
<div class="flex items-center justify-between mb-4">
|
|
166
|
+
<h3 class="text-sm font-medium text-gray-500 uppercase tracking-wide">Response</h3>
|
|
167
|
+
<button
|
|
168
|
+
type="button"
|
|
169
|
+
data-copy-json="<%= Base64.strict_encode64(JSON.pretty_generate(@execution.response)) %>"
|
|
170
|
+
class="copy-json-btn inline-flex items-center gap-1.5 px-2.5 py-1.5 text-xs font-medium text-gray-500 hover:text-gray-700 hover:bg-gray-100 rounded-md transition-colors"
|
|
171
|
+
>
|
|
172
|
+
<svg class="w-4 h-4 copy-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
173
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"/>
|
|
174
|
+
</svg>
|
|
175
|
+
<svg class="w-4 h-4 check-icon hidden" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
176
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/>
|
|
177
|
+
</svg>
|
|
178
|
+
<span>Copy</span>
|
|
179
|
+
</button>
|
|
180
|
+
</div>
|
|
181
|
+
<pre class="bg-gray-50 rounded-lg p-4 text-sm overflow-x-auto max-h-96 font-mono"><%= highlight_json(@execution.response) %></pre>
|
|
182
|
+
</div>
|
|
183
|
+
<% end %>
|
|
184
|
+
|
|
185
|
+
<!-- Metadata -->
|
|
186
|
+
<% if @execution.metadata.present? && @execution.metadata.any? %>
|
|
187
|
+
<div class="bg-white border border-gray-200 rounded-xl p-6 mb-6">
|
|
188
|
+
<div class="flex items-center justify-between mb-4">
|
|
189
|
+
<h3 class="text-sm font-medium text-gray-500 uppercase tracking-wide">Metadata</h3>
|
|
190
|
+
<button
|
|
191
|
+
type="button"
|
|
192
|
+
data-copy-json="<%= Base64.strict_encode64(JSON.pretty_generate(@execution.metadata)) %>"
|
|
193
|
+
class="copy-json-btn inline-flex items-center gap-1.5 px-2.5 py-1.5 text-xs font-medium text-gray-500 hover:text-gray-700 hover:bg-gray-100 rounded-md transition-colors"
|
|
194
|
+
>
|
|
195
|
+
<svg class="w-4 h-4 copy-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
196
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"/>
|
|
197
|
+
</svg>
|
|
198
|
+
<svg class="w-4 h-4 check-icon hidden" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
199
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/>
|
|
200
|
+
</svg>
|
|
201
|
+
<span>Copy</span>
|
|
202
|
+
</button>
|
|
203
|
+
</div>
|
|
204
|
+
<pre class="bg-gray-50 rounded-lg p-4 text-sm overflow-x-auto font-mono"><%= highlight_json(@execution.metadata) %></pre>
|
|
205
|
+
</div>
|
|
206
|
+
<% end %>
|
|
207
|
+
|
|
208
|
+
<script>
|
|
209
|
+
document.addEventListener('DOMContentLoaded', function() {
|
|
210
|
+
document.querySelectorAll('.copy-json-btn').forEach(function(button) {
|
|
211
|
+
button.addEventListener('click', function() {
|
|
212
|
+
const base64Data = this.getAttribute('data-copy-json');
|
|
213
|
+
const jsonText = atob(base64Data);
|
|
214
|
+
const span = this.querySelector('span');
|
|
215
|
+
const copyIcon = this.querySelector('.copy-icon');
|
|
216
|
+
const checkIcon = this.querySelector('.check-icon');
|
|
217
|
+
|
|
218
|
+
navigator.clipboard.writeText(jsonText).then(function() {
|
|
219
|
+
span.textContent = 'Copied!';
|
|
220
|
+
copyIcon.classList.add('hidden');
|
|
221
|
+
checkIcon.classList.remove('hidden');
|
|
222
|
+
button.classList.add('text-green-600');
|
|
223
|
+
|
|
224
|
+
setTimeout(function() {
|
|
225
|
+
span.textContent = 'Copy';
|
|
226
|
+
copyIcon.classList.remove('hidden');
|
|
227
|
+
checkIcon.classList.add('hidden');
|
|
228
|
+
button.classList.remove('text-green-600');
|
|
229
|
+
}, 2000);
|
|
230
|
+
}).catch(function(err) {
|
|
231
|
+
console.error('Failed to copy:', err);
|
|
232
|
+
span.textContent = 'Failed';
|
|
233
|
+
setTimeout(function() {
|
|
234
|
+
span.textContent = 'Copy';
|
|
235
|
+
}, 2000);
|
|
236
|
+
});
|
|
237
|
+
});
|
|
238
|
+
});
|
|
239
|
+
});
|
|
240
|
+
</script>
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
<% if executions.empty? %>
|
|
2
|
+
<p class="text-gray-500 text-center py-8">No executions found.</p>
|
|
3
|
+
<% else %>
|
|
4
|
+
<div class="overflow-x-auto">
|
|
5
|
+
<table class="min-w-full divide-y divide-gray-200">
|
|
6
|
+
<thead>
|
|
7
|
+
<tr>
|
|
8
|
+
<th scope="col" class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Status</th>
|
|
9
|
+
<th scope="col" class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Version</th>
|
|
10
|
+
<th scope="col" class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Model</th>
|
|
11
|
+
<th scope="col" class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Temp</th>
|
|
12
|
+
<th scope="col" class="px-4 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">Tokens</th>
|
|
13
|
+
<th scope="col" class="px-4 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">Cost</th>
|
|
14
|
+
<th scope="col" class="px-4 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">Duration</th>
|
|
15
|
+
<th scope="col" class="px-4 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">Time</th>
|
|
16
|
+
</tr>
|
|
17
|
+
</thead>
|
|
18
|
+
<tbody class="bg-white divide-y divide-gray-100">
|
|
19
|
+
<% executions.each do |execution| %>
|
|
20
|
+
<!-- Main Row (clickable to expand) -->
|
|
21
|
+
<tr class="hover:bg-gray-50 transition-colors cursor-pointer group"
|
|
22
|
+
onclick="toggleExecutionDetails('<%= execution.id %>')"
|
|
23
|
+
id="execution-row-<%= execution.id %>">
|
|
24
|
+
<td class="px-4 py-3 whitespace-nowrap">
|
|
25
|
+
<div class="flex items-center">
|
|
26
|
+
<svg class="w-4 h-4 mr-2 text-gray-400 transform transition-transform duration-200"
|
|
27
|
+
id="chevron-<%= execution.id %>" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
28
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/>
|
|
29
|
+
</svg>
|
|
30
|
+
<%= render "rubyllm/agents/shared/status_badge", status: execution.status %>
|
|
31
|
+
</div>
|
|
32
|
+
</td>
|
|
33
|
+
<td class="px-4 py-3 whitespace-nowrap text-sm text-gray-500">
|
|
34
|
+
v<%= execution.agent_version %>
|
|
35
|
+
</td>
|
|
36
|
+
<td class="px-4 py-3 whitespace-nowrap text-sm text-gray-500">
|
|
37
|
+
<%= execution.model_id %>
|
|
38
|
+
</td>
|
|
39
|
+
<td class="px-4 py-3 whitespace-nowrap text-sm text-gray-500">
|
|
40
|
+
<%= execution.temperature %>
|
|
41
|
+
</td>
|
|
42
|
+
<td class="px-4 py-3 whitespace-nowrap text-sm text-gray-900 text-right font-medium">
|
|
43
|
+
<%= number_to_human_short(execution.total_tokens || 0) %>
|
|
44
|
+
</td>
|
|
45
|
+
<td class="px-4 py-3 whitespace-nowrap text-sm text-gray-900 text-right font-medium">
|
|
46
|
+
<%= number_to_human_short(execution.total_cost || 0, prefix: "$", precision: 2) %>
|
|
47
|
+
</td>
|
|
48
|
+
<td class="px-4 py-3 whitespace-nowrap text-sm text-gray-900 text-right font-medium">
|
|
49
|
+
<%= number_with_delimiter(execution.duration_ms || 0) %>ms
|
|
50
|
+
</td>
|
|
51
|
+
<td class="px-4 py-3 whitespace-nowrap text-sm text-gray-500 text-right">
|
|
52
|
+
<%= time_ago_in_words(execution.created_at) %> ago
|
|
53
|
+
</td>
|
|
54
|
+
</tr>
|
|
55
|
+
|
|
56
|
+
<!-- Error Row (if applicable) -->
|
|
57
|
+
<% if execution.status_error? && execution.error_message.present? %>
|
|
58
|
+
<tr class="bg-red-50">
|
|
59
|
+
<td colspan="8" class="px-4 py-2">
|
|
60
|
+
<p class="text-xs text-red-600">
|
|
61
|
+
<span class="font-medium"><%= execution.error_class %>:</span>
|
|
62
|
+
<%= truncate(execution.error_message, length: 150) %>
|
|
63
|
+
</p>
|
|
64
|
+
</td>
|
|
65
|
+
</tr>
|
|
66
|
+
<% end %>
|
|
67
|
+
|
|
68
|
+
<!-- Expandable Details Row (hidden by default) -->
|
|
69
|
+
<tr id="execution-details-<%= execution.id %>" class="hidden bg-gray-50">
|
|
70
|
+
<td colspan="8" class="px-4 py-4">
|
|
71
|
+
<div class="grid grid-cols-1 lg:grid-cols-2 gap-4">
|
|
72
|
+
<% if execution.respond_to?(:system_prompt) && execution.system_prompt.present? %>
|
|
73
|
+
<div>
|
|
74
|
+
<h4 class="text-xs font-medium text-gray-500 uppercase mb-2">System Prompt</h4>
|
|
75
|
+
<pre class="bg-white border border-gray-200 rounded-lg p-3 text-xs overflow-x-auto max-h-48 overflow-y-auto font-mono whitespace-pre-wrap"><%= execution.system_prompt %></pre>
|
|
76
|
+
</div>
|
|
77
|
+
<% end %>
|
|
78
|
+
|
|
79
|
+
<% if execution.respond_to?(:user_prompt) && execution.user_prompt.present? %>
|
|
80
|
+
<div>
|
|
81
|
+
<h4 class="text-xs font-medium text-gray-500 uppercase mb-2">User Prompt</h4>
|
|
82
|
+
<pre class="bg-white border border-gray-200 rounded-lg p-3 text-xs overflow-x-auto max-h-48 overflow-y-auto font-mono whitespace-pre-wrap"><%= execution.user_prompt %></pre>
|
|
83
|
+
</div>
|
|
84
|
+
<% end %>
|
|
85
|
+
|
|
86
|
+
<% if execution.parameters.present? && execution.parameters.any? %>
|
|
87
|
+
<div>
|
|
88
|
+
<h4 class="text-xs font-medium text-gray-500 uppercase mb-2">Parameters</h4>
|
|
89
|
+
<pre class="bg-white border border-gray-200 rounded-lg p-3 text-xs overflow-x-auto max-h-48 overflow-y-auto font-mono"><%= JSON.pretty_generate(execution.parameters) %></pre>
|
|
90
|
+
</div>
|
|
91
|
+
<% end %>
|
|
92
|
+
|
|
93
|
+
<% if execution.response.present? && execution.response.any? %>
|
|
94
|
+
<div>
|
|
95
|
+
<h4 class="text-xs font-medium text-gray-500 uppercase mb-2">Response</h4>
|
|
96
|
+
<pre class="bg-white border border-gray-200 rounded-lg p-3 text-xs overflow-x-auto max-h-48 overflow-y-auto font-mono"><%= JSON.pretty_generate(execution.response) %></pre>
|
|
97
|
+
</div>
|
|
98
|
+
<% end %>
|
|
99
|
+
|
|
100
|
+
<% if execution.metadata.present? && execution.metadata.any? %>
|
|
101
|
+
<div>
|
|
102
|
+
<h4 class="text-xs font-medium text-gray-500 uppercase mb-2">Metadata</h4>
|
|
103
|
+
<pre class="bg-white border border-gray-200 rounded-lg p-3 text-xs overflow-x-auto max-h-48 overflow-y-auto font-mono"><%= JSON.pretty_generate(execution.metadata) %></pre>
|
|
104
|
+
</div>
|
|
105
|
+
<% end %>
|
|
106
|
+
</div>
|
|
107
|
+
|
|
108
|
+
<!-- View Full Details Link -->
|
|
109
|
+
<div class="mt-3 pt-3 border-t border-gray-200">
|
|
110
|
+
<%= link_to ruby_llm_agents.execution_path(execution), class: "text-sm text-blue-600 hover:underline", onclick: "event.stopPropagation();" do %>
|
|
111
|
+
View Full Details →
|
|
112
|
+
<% end %>
|
|
113
|
+
</div>
|
|
114
|
+
</td>
|
|
115
|
+
</tr>
|
|
116
|
+
<% end %>
|
|
117
|
+
</tbody>
|
|
118
|
+
</table>
|
|
119
|
+
</div>
|
|
120
|
+
|
|
121
|
+
<% if pagination[:total_pages] > 1 %>
|
|
122
|
+
<%
|
|
123
|
+
current_page = pagination[:current_page]
|
|
124
|
+
total_pages = pagination[:total_pages]
|
|
125
|
+
total_count = pagination[:total_count]
|
|
126
|
+
per_page = pagination[:per_page]
|
|
127
|
+
|
|
128
|
+
from_record = ((current_page - 1) * per_page) + 1
|
|
129
|
+
to_record = [current_page * per_page, total_count].min
|
|
130
|
+
%>
|
|
131
|
+
<div class="mt-4 flex items-center justify-between border-t border-gray-100 pt-4">
|
|
132
|
+
<p class="text-sm text-gray-500">
|
|
133
|
+
Showing <%= from_record %>-<%= to_record %> of <%= number_with_delimiter(total_count) %> executions
|
|
134
|
+
</p>
|
|
135
|
+
<nav class="flex items-center space-x-1">
|
|
136
|
+
<% if current_page > 1 %>
|
|
137
|
+
<%= link_to "Previous", url_for(request.query_parameters.merge(page: current_page - 1)), data: { turbo_frame: "executions_table" }, 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" %>
|
|
138
|
+
<% else %>
|
|
139
|
+
<span class="px-3 py-1.5 text-sm font-medium text-gray-400 bg-gray-100 border border-gray-200 rounded-md cursor-not-allowed">Previous</span>
|
|
140
|
+
<% end %>
|
|
141
|
+
|
|
142
|
+
<%
|
|
143
|
+
# Calculate which page numbers to show
|
|
144
|
+
window = 2
|
|
145
|
+
left_edge = 1
|
|
146
|
+
right_edge = 1
|
|
147
|
+
|
|
148
|
+
pages_to_show = []
|
|
149
|
+
(1..total_pages).each do |page|
|
|
150
|
+
if page <= left_edge ||
|
|
151
|
+
page > total_pages - right_edge ||
|
|
152
|
+
(page >= current_page - window && page <= current_page + window)
|
|
153
|
+
pages_to_show << page
|
|
154
|
+
elsif pages_to_show.last != :gap
|
|
155
|
+
pages_to_show << :gap
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
%>
|
|
159
|
+
|
|
160
|
+
<% pages_to_show.each do |page| %>
|
|
161
|
+
<% if page == :gap %>
|
|
162
|
+
<span class="px-2 py-1.5 text-sm text-gray-500">...</span>
|
|
163
|
+
<% elsif page == current_page %>
|
|
164
|
+
<span class="px-3 py-1.5 text-sm font-medium text-white bg-blue-600 border border-blue-600 rounded-md"><%= page %></span>
|
|
165
|
+
<% else %>
|
|
166
|
+
<%= link_to page, url_for(request.query_parameters.merge(page: page)), data: { turbo_frame: "executions_table" }, 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" %>
|
|
167
|
+
<% end %>
|
|
168
|
+
<% end %>
|
|
169
|
+
|
|
170
|
+
<% if current_page < total_pages %>
|
|
171
|
+
<%= link_to "Next", url_for(request.query_parameters.merge(page: current_page + 1)), data: { turbo_frame: "executions_table" }, 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" %>
|
|
172
|
+
<% else %>
|
|
173
|
+
<span class="px-3 py-1.5 text-sm font-medium text-gray-400 bg-gray-100 border border-gray-200 rounded-md cursor-not-allowed">Next</span>
|
|
174
|
+
<% end %>
|
|
175
|
+
</nav>
|
|
176
|
+
</div>
|
|
177
|
+
<% end %>
|
|
178
|
+
<% end %>
|
|
179
|
+
|
|
180
|
+
<script>
|
|
181
|
+
function toggleExecutionDetails(id) {
|
|
182
|
+
const detailsRow = document.getElementById(`execution-details-${id}`);
|
|
183
|
+
const chevron = document.getElementById(`chevron-${id}`);
|
|
184
|
+
|
|
185
|
+
if (detailsRow.classList.contains('hidden')) {
|
|
186
|
+
detailsRow.classList.remove('hidden');
|
|
187
|
+
chevron.classList.add('rotate-90');
|
|
188
|
+
} else {
|
|
189
|
+
detailsRow.classList.add('hidden');
|
|
190
|
+
chevron.classList.remove('rotate-90');
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
</script>
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
<div class="bg-white border border-gray-200 rounded-xl p-4">
|
|
2
|
+
<div class="flex items-center justify-between">
|
|
3
|
+
<p class="text-xs text-gray-500 uppercase tracking-wide font-medium"><%= title %></p>
|
|
4
|
+
<span class="<%= local_assigns[:icon_color] || 'text-gray-400' %>">
|
|
5
|
+
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
6
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="<%= icon %>"/>
|
|
7
|
+
</svg>
|
|
8
|
+
</span>
|
|
9
|
+
</div>
|
|
10
|
+
<p class="text-xl font-semibold <%= local_assigns[:value_color] || 'text-gray-900' %> mt-2"><%= value %></p>
|
|
11
|
+
<% if local_assigns[:subtitle].present? %>
|
|
12
|
+
<p class="text-xs text-gray-400"><%= subtitle %></p>
|
|
13
|
+
<% end %>
|
|
14
|
+
</div>
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
<%
|
|
2
|
+
# Status badge with icon and color coding
|
|
3
|
+
# Usage: render "rubyllm/agents/shared/status_badge", status: execution.status
|
|
4
|
+
# Options: size: :sm (default), :md, :lg
|
|
5
|
+
|
|
6
|
+
size = local_assigns[:size] || :sm
|
|
7
|
+
|
|
8
|
+
config = case status.to_s
|
|
9
|
+
when "running"
|
|
10
|
+
{
|
|
11
|
+
bg: "bg-blue-50",
|
|
12
|
+
text: "text-blue-700",
|
|
13
|
+
dot: "bg-blue-500",
|
|
14
|
+
icon: "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",
|
|
15
|
+
animate: true
|
|
16
|
+
}
|
|
17
|
+
when "success"
|
|
18
|
+
{
|
|
19
|
+
bg: "bg-green-50",
|
|
20
|
+
text: "text-green-700",
|
|
21
|
+
dot: "bg-green-500",
|
|
22
|
+
icon: "M5 13l4 4L19 7",
|
|
23
|
+
animate: false
|
|
24
|
+
}
|
|
25
|
+
when "error"
|
|
26
|
+
{
|
|
27
|
+
bg: "bg-red-50",
|
|
28
|
+
text: "text-red-700",
|
|
29
|
+
dot: "bg-red-500",
|
|
30
|
+
icon: "M6 18L18 6M6 6l12 12",
|
|
31
|
+
animate: false
|
|
32
|
+
}
|
|
33
|
+
when "timeout"
|
|
34
|
+
{
|
|
35
|
+
bg: "bg-yellow-50",
|
|
36
|
+
text: "text-yellow-700",
|
|
37
|
+
dot: "bg-yellow-500",
|
|
38
|
+
icon: "M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z",
|
|
39
|
+
animate: false
|
|
40
|
+
}
|
|
41
|
+
else
|
|
42
|
+
{
|
|
43
|
+
bg: "bg-gray-50",
|
|
44
|
+
text: "text-gray-700",
|
|
45
|
+
dot: "bg-gray-500",
|
|
46
|
+
icon: "M8.228 9c.549-1.165 2.03-2 3.772-2 2.21 0 4 1.343 4 3 0 1.4-1.278 2.575-3.006 2.907-.542.104-.994.54-.994 1.093m0 3h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z",
|
|
47
|
+
animate: false
|
|
48
|
+
}
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
size_classes = case size
|
|
52
|
+
when :lg
|
|
53
|
+
{ badge: "px-3 py-1.5", icon: "w-4 h-4", text: "text-sm" }
|
|
54
|
+
when :md
|
|
55
|
+
{ badge: "px-2.5 py-1", icon: "w-3.5 h-3.5", text: "text-sm" }
|
|
56
|
+
else # :sm
|
|
57
|
+
{ badge: "px-2 py-0.5", icon: "w-3 h-3", text: "text-xs" }
|
|
58
|
+
end
|
|
59
|
+
%>
|
|
60
|
+
<span class="inline-flex items-center gap-1.5 <%= size_classes[:badge] %> rounded-full <%= config[:bg] %> <%= config[:text] %> font-medium">
|
|
61
|
+
<svg class="<%= size_classes[:icon] %> <%= config[:animate] ? 'animate-spin' : '' %>" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
62
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="<%= config[:icon] %>"/>
|
|
63
|
+
</svg>
|
|
64
|
+
<span class="<%= size_classes[:text] %>"><%= status.to_s.capitalize %></span>
|
|
65
|
+
</span>
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
<%
|
|
2
|
+
# Simple status dot indicator
|
|
3
|
+
# Usage: render "rubyllm/agents/shared/status_dot", status: execution.status
|
|
4
|
+
|
|
5
|
+
config = case status.to_s
|
|
6
|
+
when "running"
|
|
7
|
+
{ color: "bg-blue-500", animate: true }
|
|
8
|
+
when "success"
|
|
9
|
+
{ color: "bg-green-500", animate: false }
|
|
10
|
+
when "error"
|
|
11
|
+
{ color: "bg-red-500", animate: false }
|
|
12
|
+
when "timeout"
|
|
13
|
+
{ color: "bg-yellow-500", animate: false }
|
|
14
|
+
else
|
|
15
|
+
{ color: "bg-gray-500", animate: false }
|
|
16
|
+
end
|
|
17
|
+
%>
|
|
18
|
+
<span class="w-2.5 h-2.5 rounded-full <%= config[:color] %> <%= config[:animate] ? 'animate-pulse' : '' %> flex-shrink-0"></span>
|
data/config/routes.rb
ADDED