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.
Files changed (51) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE.txt +21 -0
  3. data/README.md +898 -0
  4. data/app/channels/ruby_llm/agents/executions_channel.rb +23 -0
  5. data/app/controllers/ruby_llm/agents/agents_controller.rb +100 -0
  6. data/app/controllers/ruby_llm/agents/application_controller.rb +20 -0
  7. data/app/controllers/ruby_llm/agents/dashboard_controller.rb +34 -0
  8. data/app/controllers/ruby_llm/agents/executions_controller.rb +93 -0
  9. data/app/helpers/ruby_llm/agents/application_helper.rb +149 -0
  10. data/app/javascript/ruby_llm/agents/controllers/filter_controller.js +56 -0
  11. data/app/javascript/ruby_llm/agents/controllers/index.js +12 -0
  12. data/app/javascript/ruby_llm/agents/controllers/refresh_controller.js +83 -0
  13. data/app/models/ruby_llm/agents/execution/analytics.rb +166 -0
  14. data/app/models/ruby_llm/agents/execution/metrics.rb +89 -0
  15. data/app/models/ruby_llm/agents/execution/scopes.rb +81 -0
  16. data/app/models/ruby_llm/agents/execution.rb +81 -0
  17. data/app/services/ruby_llm/agents/agent_registry.rb +112 -0
  18. data/app/views/layouts/rubyllm/agents/application.html.erb +276 -0
  19. data/app/views/rubyllm/agents/agents/index.html.erb +89 -0
  20. data/app/views/rubyllm/agents/agents/show.html.erb +562 -0
  21. data/app/views/rubyllm/agents/dashboard/_execution_item.html.erb +48 -0
  22. data/app/views/rubyllm/agents/dashboard/index.html.erb +121 -0
  23. data/app/views/rubyllm/agents/executions/_execution.html.erb +64 -0
  24. data/app/views/rubyllm/agents/executions/_filters.html.erb +172 -0
  25. data/app/views/rubyllm/agents/executions/_list.html.erb +229 -0
  26. data/app/views/rubyllm/agents/executions/index.html.erb +83 -0
  27. data/app/views/rubyllm/agents/executions/index.turbo_stream.erb +4 -0
  28. data/app/views/rubyllm/agents/executions/show.html.erb +240 -0
  29. data/app/views/rubyllm/agents/shared/_executions_table.html.erb +193 -0
  30. data/app/views/rubyllm/agents/shared/_stat_card.html.erb +14 -0
  31. data/app/views/rubyllm/agents/shared/_status_badge.html.erb +65 -0
  32. data/app/views/rubyllm/agents/shared/_status_dot.html.erb +18 -0
  33. data/config/routes.rb +13 -0
  34. data/lib/generators/ruby_llm_agents/agent_generator.rb +79 -0
  35. data/lib/generators/ruby_llm_agents/install_generator.rb +89 -0
  36. data/lib/generators/ruby_llm_agents/templates/add_prompts_migration.rb.tt +12 -0
  37. data/lib/generators/ruby_llm_agents/templates/agent.rb.tt +46 -0
  38. data/lib/generators/ruby_llm_agents/templates/application_agent.rb.tt +22 -0
  39. data/lib/generators/ruby_llm_agents/templates/initializer.rb.tt +36 -0
  40. data/lib/generators/ruby_llm_agents/templates/migration.rb.tt +66 -0
  41. data/lib/generators/ruby_llm_agents/upgrade_generator.rb +59 -0
  42. data/lib/ruby_llm/agents/base.rb +271 -0
  43. data/lib/ruby_llm/agents/configuration.rb +36 -0
  44. data/lib/ruby_llm/agents/engine.rb +32 -0
  45. data/lib/ruby_llm/agents/execution_logger_job.rb +59 -0
  46. data/lib/ruby_llm/agents/inflections.rb +13 -0
  47. data/lib/ruby_llm/agents/instrumentation.rb +245 -0
  48. data/lib/ruby_llm/agents/version.rb +7 -0
  49. data/lib/ruby_llm/agents.rb +26 -0
  50. data/lib/ruby_llm-agents.rb +3 -0
  51. 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 &rarr;
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
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ RubyLLM::Agents::Engine.routes.draw do
4
+ root to: "dashboard#index"
5
+
6
+ resources :agents, only: [:index, :show]
7
+
8
+ resources :executions, only: [:index, :show] do
9
+ collection do
10
+ get :search
11
+ end
12
+ end
13
+ end