ruby_llm-agents 0.2.4 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +413 -0
- data/app/channels/ruby_llm/agents/executions_channel.rb +24 -1
- data/app/controllers/concerns/ruby_llm/agents/filterable.rb +81 -0
- data/app/controllers/concerns/ruby_llm/agents/paginatable.rb +51 -0
- data/app/controllers/ruby_llm/agents/agents_controller.rb +228 -59
- data/app/controllers/ruby_llm/agents/dashboard_controller.rb +167 -12
- data/app/controllers/ruby_llm/agents/executions_controller.rb +189 -31
- data/app/controllers/ruby_llm/agents/settings_controller.rb +20 -0
- data/app/helpers/ruby_llm/agents/application_helper.rb +307 -7
- data/app/models/ruby_llm/agents/execution/analytics.rb +224 -20
- data/app/models/ruby_llm/agents/execution/metrics.rb +41 -25
- data/app/models/ruby_llm/agents/execution/scopes.rb +234 -14
- data/app/models/ruby_llm/agents/execution.rb +259 -16
- data/app/services/ruby_llm/agents/agent_registry.rb +49 -12
- data/app/views/layouts/rubyllm/agents/application.html.erb +351 -85
- data/app/views/rubyllm/agents/agents/_version_comparison.html.erb +186 -0
- data/app/views/rubyllm/agents/agents/show.html.erb +233 -10
- data/app/views/rubyllm/agents/dashboard/_action_center.html.erb +62 -0
- data/app/views/rubyllm/agents/dashboard/_alerts_feed.html.erb +62 -0
- data/app/views/rubyllm/agents/dashboard/_breaker_strip.html.erb +47 -0
- data/app/views/rubyllm/agents/dashboard/_budgets_bar.html.erb +165 -0
- data/app/views/rubyllm/agents/dashboard/_now_strip.html.erb +10 -0
- data/app/views/rubyllm/agents/dashboard/_now_strip_values.html.erb +71 -0
- data/app/views/rubyllm/agents/dashboard/index.html.erb +215 -109
- data/app/views/rubyllm/agents/executions/_filters.html.erb +152 -155
- data/app/views/rubyllm/agents/executions/_list.html.erb +103 -12
- data/app/views/rubyllm/agents/executions/dry_run.html.erb +149 -0
- data/app/views/rubyllm/agents/executions/index.html.erb +17 -72
- data/app/views/rubyllm/agents/executions/index.turbo_stream.erb +16 -2
- data/app/views/rubyllm/agents/executions/show.html.erb +693 -14
- data/app/views/rubyllm/agents/settings/show.html.erb +369 -0
- data/app/views/rubyllm/agents/shared/_filter_dropdown.html.erb +121 -0
- data/app/views/rubyllm/agents/shared/_select_dropdown.html.erb +85 -0
- data/config/routes.rb +7 -0
- data/lib/generators/ruby_llm_agents/templates/add_attempts_migration.rb.tt +27 -0
- data/lib/generators/ruby_llm_agents/templates/add_caching_migration.rb.tt +23 -0
- data/lib/generators/ruby_llm_agents/templates/add_finish_reason_migration.rb.tt +19 -0
- data/lib/generators/ruby_llm_agents/templates/add_routing_migration.rb.tt +19 -0
- data/lib/generators/ruby_llm_agents/templates/add_streaming_migration.rb.tt +8 -0
- data/lib/generators/ruby_llm_agents/templates/add_tracing_migration.rb.tt +34 -0
- data/lib/generators/ruby_llm_agents/templates/agent.rb.tt +66 -4
- data/lib/generators/ruby_llm_agents/templates/application_agent.rb.tt +53 -6
- data/lib/generators/ruby_llm_agents/templates/initializer.rb.tt +143 -8
- data/lib/generators/ruby_llm_agents/templates/migration.rb.tt +38 -1
- data/lib/generators/ruby_llm_agents/upgrade_generator.rb +78 -0
- data/lib/ruby_llm/agents/alert_manager.rb +207 -0
- data/lib/ruby_llm/agents/attempt_tracker.rb +295 -0
- data/lib/ruby_llm/agents/base.rb +597 -112
- data/lib/ruby_llm/agents/budget_tracker.rb +360 -0
- data/lib/ruby_llm/agents/circuit_breaker.rb +197 -0
- data/lib/ruby_llm/agents/configuration.rb +279 -1
- data/lib/ruby_llm/agents/engine.rb +58 -6
- data/lib/ruby_llm/agents/execution_logger_job.rb +17 -6
- data/lib/ruby_llm/agents/inflections.rb +13 -2
- data/lib/ruby_llm/agents/instrumentation.rb +538 -87
- data/lib/ruby_llm/agents/redactor.rb +130 -0
- data/lib/ruby_llm/agents/reliability.rb +185 -0
- data/lib/ruby_llm/agents/version.rb +3 -1
- data/lib/ruby_llm/agents.rb +52 -0
- metadata +41 -2
- data/app/controllers/ruby_llm/agents/application_controller.rb +0 -37
|
@@ -1,171 +1,168 @@
|
|
|
1
1
|
<%
|
|
2
|
-
|
|
2
|
+
# Parse filter params
|
|
3
3
|
selected_agents = params[:agent_types].present? ? (params[:agent_types].is_a?(Array) ? params[:agent_types] : params[:agent_types].split(",")) : []
|
|
4
4
|
selected_statuses = params[:statuses].present? ? (params[:statuses].is_a?(Array) ? params[:statuses] : params[:statuses].split(",")) : []
|
|
5
|
+
selected_models = params[:model_ids].present? ? (params[:model_ids].is_a?(Array) ? params[:model_ids] : params[:model_ids].split(",")) : []
|
|
6
|
+
|
|
7
|
+
has_filters = selected_agents.any? || selected_statuses.any? || params[:days].present? || selected_models.any?
|
|
8
|
+
active_filter_count = [
|
|
9
|
+
selected_agents.any? ? 1 : 0,
|
|
10
|
+
selected_statuses.any? ? 1 : 0,
|
|
11
|
+
params[:days].present? ? 1 : 0,
|
|
12
|
+
selected_models.any? ? 1 : 0
|
|
13
|
+
].sum
|
|
14
|
+
|
|
15
|
+
status_options = [
|
|
16
|
+
{ value: "success", label: "Success", color: "bg-green-500" },
|
|
17
|
+
{ value: "error", label: "Error", color: "bg-red-500" },
|
|
18
|
+
{ value: "running", label: "Running", color: "bg-blue-500" },
|
|
19
|
+
{ value: "timeout", label: "Timeout", color: "bg-yellow-500" }
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
time_options = [
|
|
23
|
+
{ value: "", label: "All Time" },
|
|
24
|
+
{ value: "1", label: "Today" },
|
|
25
|
+
{ value: "7", label: "Last 7 Days" },
|
|
26
|
+
{ value: "30", label: "Last 30 Days" }
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
agent_options = agent_types.map { |t| { value: t, label: t.gsub(/Agent$/, '') } }
|
|
30
|
+
|
|
31
|
+
# Build model options - extract short name from model_id
|
|
32
|
+
model_options = local_assigns[:model_ids]&.map do |m|
|
|
33
|
+
short_name = m.split("/").last.split(":").first # Handle "provider/model:version" format
|
|
34
|
+
{ value: m, label: short_name }
|
|
35
|
+
end || []
|
|
5
36
|
%>
|
|
6
|
-
<div class="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-4 mb-6"
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
37
|
+
<div id="filters-container" class="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-4 mb-6"
|
|
38
|
+
x-data="{ mobileOpen: false }">
|
|
39
|
+
<%= form_with url: ruby_llm_agents.executions_path, method: :get,
|
|
40
|
+
data: { turbo_stream: true, turbo_action: "advance" },
|
|
41
|
+
id: "filters-form" do |f| %>
|
|
42
|
+
|
|
43
|
+
<%# Mobile: Toggle button (only shows on small screens) %>
|
|
44
|
+
<button type="button" @click="mobileOpen = !mobileOpen"
|
|
45
|
+
class="md:hidden w-full flex items-center justify-between gap-2 px-3 py-2 text-sm
|
|
46
|
+
bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-600 rounded-lg
|
|
47
|
+
<%= has_filters ? 'ring-2 ring-blue-500 dark:ring-offset-gray-900' : '' %>">
|
|
48
|
+
<div class="flex items-center gap-2">
|
|
49
|
+
<svg class="w-4 h-4 text-gray-400 dark:text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
50
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 4a1 1 0 011-1h16a1 1 0 011 1v2.586a1 1 0 01-.293.707l-6.414 6.414a1 1 0 00-.293.707V17l-4 4v-6.586a1 1 0 00-.293-.707L3.293 7.293A1 1 0 013 6.586V4z"/>
|
|
51
|
+
</svg>
|
|
52
|
+
<span class="text-gray-700 dark:text-gray-200">Filters</span>
|
|
53
|
+
<% if active_filter_count > 0 %>
|
|
54
|
+
<span id="mobile-filter-badge" class="inline-flex items-center justify-center w-5 h-5 text-xs font-medium text-white bg-blue-600 rounded-full">
|
|
55
|
+
<%= active_filter_count %>
|
|
23
56
|
</span>
|
|
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 dark:bg-gray-800 rounded-lg shadow-lg border border-gray-200 dark:border-gray-700 py-1">
|
|
29
|
-
<div class="px-3 py-2 border-b border-gray-100 dark:border-gray-700">
|
|
30
|
-
<label class="flex items-center gap-2 cursor-pointer">
|
|
31
|
-
<input type="checkbox" class="select-all-checkbox rounded border-gray-300 dark:border-gray-600 text-blue-600 focus:ring-blue-500 dark:bg-gray-700" onchange="toggleAllOptions(this, 'agent_types')" <%= selected_agents.empty? ? 'checked' : '' %>>
|
|
32
|
-
<span class="text-sm font-medium text-gray-700 dark:text-gray-200">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 dark:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-700 cursor-pointer">
|
|
37
|
-
<input type="checkbox" name="agent_types[]" value="<%= agent_type %>" class="filter-checkbox rounded border-gray-300 dark:border-gray-600 text-blue-600 focus:ring-blue-500 dark:bg-gray-700" 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>
|
|
57
|
+
<% end %>
|
|
45
58
|
</div>
|
|
59
|
+
<svg class="w-4 h-4 text-gray-400 dark:text-gray-500 transition-transform duration-200" :class="{ 'rotate-180': mobileOpen }" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
60
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/>
|
|
61
|
+
</svg>
|
|
62
|
+
</button>
|
|
46
63
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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 dark:bg-gray-800 rounded-lg shadow-lg border border-gray-200 dark:border-gray-700 py-1">
|
|
76
|
-
<div class="px-3 py-2 border-b border-gray-100 dark:border-gray-700">
|
|
77
|
-
<label class="flex items-center gap-2 cursor-pointer">
|
|
78
|
-
<input type="checkbox" class="select-all-checkbox rounded border-gray-300 dark:border-gray-600 text-blue-600 focus:ring-blue-500 dark:bg-gray-700" onchange="toggleAllOptions(this, 'statuses')" <%= selected_statuses.empty? ? 'checked' : '' %>>
|
|
79
|
-
<span class="text-sm font-medium text-gray-700 dark:text-gray-200">All Statuses</span>
|
|
80
|
-
</label>
|
|
81
|
-
</div>
|
|
82
|
-
<label class="flex items-center gap-2 px-3 py-2 text-sm text-gray-700 dark:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-700 cursor-pointer">
|
|
83
|
-
<input type="checkbox" name="statuses[]" value="success" class="filter-checkbox rounded border-gray-300 dark:border-gray-600 text-blue-600 focus:ring-blue-500 dark:bg-gray-700" 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 dark:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-700 cursor-pointer">
|
|
88
|
-
<input type="checkbox" name="statuses[]" value="error" class="filter-checkbox rounded border-gray-300 dark:border-gray-600 text-blue-600 focus:ring-blue-500 dark:bg-gray-700" 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 dark:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-700 cursor-pointer">
|
|
93
|
-
<input type="checkbox" name="statuses[]" value="running" class="filter-checkbox rounded border-gray-300 dark:border-gray-600 text-blue-600 focus:ring-blue-500 dark:bg-gray-700" 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 dark:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-700 cursor-pointer">
|
|
98
|
-
<input type="checkbox" name="statuses[]" value="timeout" class="filter-checkbox rounded border-gray-300 dark:border-gray-600 text-blue-600 focus:ring-blue-500 dark:bg-gray-700" 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>
|
|
64
|
+
<%# Filters container: responsive layout %>
|
|
65
|
+
<%# Mobile: hidden by default, toggle with Alpine %>
|
|
66
|
+
<%# Desktop: always visible %>
|
|
67
|
+
<div x-cloak class="mt-3 md:mt-0"
|
|
68
|
+
:class="{ 'hidden md:block': !mobileOpen, 'block': mobileOpen }">
|
|
69
|
+
|
|
70
|
+
<%# Responsive flex: column on mobile, row on desktop %>
|
|
71
|
+
<div class="flex flex-col md:flex-row md:items-center gap-3">
|
|
72
|
+
<%# Status Filter %>
|
|
73
|
+
<div class="md:w-auto">
|
|
74
|
+
<%= render "rubyllm/agents/shared/filter_dropdown",
|
|
75
|
+
name: "statuses[]",
|
|
76
|
+
filter_id: "statuses",
|
|
77
|
+
label: "Status",
|
|
78
|
+
all_label: "All Statuses",
|
|
79
|
+
options: status_options,
|
|
80
|
+
selected: selected_statuses,
|
|
81
|
+
width: "w-44",
|
|
82
|
+
full_width: true %>
|
|
102
83
|
</div>
|
|
103
|
-
</div>
|
|
104
84
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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 dark:bg-gray-800 rounded-lg shadow-lg border border-gray-200 dark:border-gray-700 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 dark:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-700 <%= params[:days].blank? ? 'bg-blue-50 dark:bg-blue-900/50 text-blue-700 dark:text-blue-300' : '' %>">
|
|
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 dark:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-700 <%= params[:days] == '1' ? 'bg-blue-50 dark:bg-blue-900/50 text-blue-700 dark:text-blue-300' : '' %>">
|
|
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 dark:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-700 <%= params[:days] == '7' ? 'bg-blue-50 dark:bg-blue-900/50 text-blue-700 dark:text-blue-300' : '' %>">
|
|
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 dark:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-700 <%= params[:days] == '30' ? 'bg-blue-50 dark:bg-blue-900/50 text-blue-700 dark:text-blue-300' : '' %>">
|
|
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>
|
|
85
|
+
<%# Time Range Filter %>
|
|
86
|
+
<div class="md:w-auto">
|
|
87
|
+
<%= render "rubyllm/agents/shared/select_dropdown",
|
|
88
|
+
name: "days",
|
|
89
|
+
filter_id: "days",
|
|
90
|
+
options: time_options,
|
|
91
|
+
selected: params[:days],
|
|
92
|
+
icon: "M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z",
|
|
93
|
+
width: "w-40",
|
|
94
|
+
full_width: true %>
|
|
148
95
|
</div>
|
|
149
|
-
<%= f.hidden_field :days, value: params[:days], id: "filter_days" %>
|
|
150
|
-
</div>
|
|
151
96
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
97
|
+
<%# Model Filter %>
|
|
98
|
+
<% if model_options.any? %>
|
|
99
|
+
<div class="md:w-auto">
|
|
100
|
+
<%= render "rubyllm/agents/shared/filter_dropdown",
|
|
101
|
+
name: "model_ids[]",
|
|
102
|
+
filter_id: "model_ids",
|
|
103
|
+
label: "Model",
|
|
104
|
+
all_label: "All Models",
|
|
105
|
+
options: model_options,
|
|
106
|
+
selected: selected_models,
|
|
107
|
+
icon: "M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z",
|
|
108
|
+
full_width: true %>
|
|
109
|
+
</div>
|
|
159
110
|
<% end %>
|
|
160
|
-
<% end %>
|
|
161
111
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
112
|
+
<%# Agent Types Filter %>
|
|
113
|
+
<% if agent_types.any? %>
|
|
114
|
+
<div class="md:w-auto">
|
|
115
|
+
<%= render "rubyllm/agents/shared/filter_dropdown",
|
|
116
|
+
name: "agent_types[]",
|
|
117
|
+
filter_id: "agent_types",
|
|
118
|
+
label: "Agents",
|
|
119
|
+
all_label: "All Agents",
|
|
120
|
+
options: agent_options,
|
|
121
|
+
selected: selected_agents,
|
|
122
|
+
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",
|
|
123
|
+
width: "w-52",
|
|
124
|
+
full_width: true %>
|
|
125
|
+
</div>
|
|
126
|
+
<% end %>
|
|
127
|
+
|
|
128
|
+
<%# Spacer (desktop only) %>
|
|
129
|
+
<div class="hidden md:block flex-1"></div>
|
|
130
|
+
|
|
131
|
+
<%# Stats %>
|
|
132
|
+
<div id="filter-stats" class="flex items-center justify-between md:justify-end gap-3 py-2 px-3 md:p-0 bg-gray-50 md:bg-transparent dark:bg-gray-700/50 md:dark:bg-transparent rounded-lg md:rounded-none">
|
|
133
|
+
<span class="text-xs text-gray-500 dark:text-gray-400 md:hidden">Results</span>
|
|
134
|
+
<div class="flex items-center gap-3 text-xs text-gray-500 dark:text-gray-400">
|
|
135
|
+
<span class="tabular-nums"><%= number_to_human_short(filter_stats[:total_count]) %><span class="md:hidden"> executions</span></span>
|
|
136
|
+
<span class="text-gray-300 dark:text-gray-600">|</span>
|
|
137
|
+
<span class="tabular-nums"><%= number_to_human_short(filter_stats[:total_cost], prefix: "$", precision: 2) %></span>
|
|
138
|
+
<span class="hidden md:inline text-gray-300 dark:text-gray-600">|</span>
|
|
139
|
+
<span class="hidden md:inline tabular-nums"><%= number_to_human_short(filter_stats[:total_tokens]) %>t</span>
|
|
140
|
+
</div>
|
|
141
|
+
</div>
|
|
142
|
+
|
|
143
|
+
<%# Actions %>
|
|
144
|
+
<div class="flex items-center gap-2 md:gap-1">
|
|
145
|
+
<% if has_filters %>
|
|
146
|
+
<%= link_to ruby_llm_agents.executions_path,
|
|
147
|
+
data: { turbo_stream: true, turbo_action: "advance" },
|
|
148
|
+
class: "flex-1 md:flex-initial flex items-center justify-center gap-2 px-3 py-2 md:p-2 text-sm md:text-base text-red-600 md:text-gray-400 dark:text-red-400 md:dark:text-gray-400 bg-red-50 md:bg-transparent dark:bg-red-900/20 md:dark:bg-transparent hover:text-red-500 hover:bg-red-100 md:hover:bg-red-50 dark:hover:bg-red-900/30 md:dark:hover:bg-red-900/20 rounded-lg transition-colors",
|
|
149
|
+
title: "Clear filters" do %>
|
|
150
|
+
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
151
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
|
|
152
|
+
</svg>
|
|
153
|
+
<span class="md:hidden">Clear</span>
|
|
154
|
+
<% end %>
|
|
155
|
+
<% end %>
|
|
156
|
+
<%= link_to ruby_llm_agents.export_executions_path(agent_types: selected_agents.presence, statuses: selected_statuses.presence, days: params[:days].presence, model_ids: selected_models.presence),
|
|
157
|
+
class: "flex-1 md:flex-initial flex items-center justify-center gap-2 px-3 py-2 md:p-2 text-sm md:text-base text-gray-600 md:text-gray-400 dark:text-gray-300 md:dark:text-gray-400 bg-gray-50 md:bg-transparent dark:bg-gray-700 md:dark:bg-transparent hover:text-gray-600 md:hover:text-gray-600 dark:hover:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-600 md:dark:hover:bg-gray-700 rounded-lg transition-colors",
|
|
158
|
+
title: "Export CSV",
|
|
159
|
+
data: { turbo: false } do %>
|
|
160
|
+
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
161
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/>
|
|
162
|
+
</svg>
|
|
163
|
+
<span class="md:hidden">Export</span>
|
|
164
|
+
<% end %>
|
|
165
|
+
</div>
|
|
169
166
|
</div>
|
|
170
167
|
</div>
|
|
171
168
|
<% end %>
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
<div class="bg-white dark:bg-gray-800 rounded-lg shadow overflow-hidden">
|
|
1
|
+
<div id="executions-list" class="bg-white dark:bg-gray-800 rounded-lg shadow overflow-hidden">
|
|
2
2
|
<% if executions.empty? %>
|
|
3
3
|
<div class="px-6 py-12 text-center text-gray-500 dark:text-gray-400">
|
|
4
4
|
<p class="text-lg">No executions found</p>
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
<table class="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
|
|
10
10
|
<thead class="bg-gray-50 dark:bg-gray-900">
|
|
11
11
|
<tr>
|
|
12
|
+
<th scope="col" class="px-2 py-3 w-8"></th>
|
|
12
13
|
<th
|
|
13
14
|
scope="col"
|
|
14
15
|
class="
|
|
@@ -39,6 +40,16 @@
|
|
|
39
40
|
Version
|
|
40
41
|
</th>
|
|
41
42
|
|
|
43
|
+
<th
|
|
44
|
+
scope="col"
|
|
45
|
+
class="
|
|
46
|
+
px-4 py-3 text-center text-xs font-medium text-gray-500 dark:text-gray-400
|
|
47
|
+
uppercase tracking-wider
|
|
48
|
+
"
|
|
49
|
+
>
|
|
50
|
+
Attempts
|
|
51
|
+
</th>
|
|
52
|
+
|
|
42
53
|
<th
|
|
43
54
|
scope="col"
|
|
44
55
|
class="
|
|
@@ -83,29 +94,61 @@
|
|
|
83
94
|
|
|
84
95
|
<tbody class="bg-white dark:bg-gray-800 divide-y divide-gray-100 dark:divide-gray-700">
|
|
85
96
|
<% executions.each do |execution| %>
|
|
97
|
+
<% has_attempts = execution.attempts.present? && execution.attempts.size > 0 %>
|
|
86
98
|
<tr
|
|
87
|
-
class="hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors
|
|
88
|
-
|
|
99
|
+
class="hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors"
|
|
100
|
+
id="execution-row-<%= execution.id %>"
|
|
89
101
|
>
|
|
90
|
-
|
|
102
|
+
<!-- Expand/Collapse Button -->
|
|
103
|
+
<td class="px-2 py-3 whitespace-nowrap">
|
|
104
|
+
<% if has_attempts %>
|
|
105
|
+
<button
|
|
106
|
+
type="button"
|
|
107
|
+
onclick="toggleAttempts(<%= execution.id %>); event.stopPropagation();"
|
|
108
|
+
class="expand-btn p-1 rounded hover:bg-gray-200 dark:hover:bg-gray-600 transition-colors"
|
|
109
|
+
data-execution-id="<%= execution.id %>"
|
|
110
|
+
aria-expanded="false"
|
|
111
|
+
aria-label="Toggle attempts"
|
|
112
|
+
>
|
|
113
|
+
<svg class="w-4 h-4 text-gray-500 dark:text-gray-400 transform transition-transform" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
114
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/>
|
|
115
|
+
</svg>
|
|
116
|
+
</button>
|
|
117
|
+
<% end %>
|
|
118
|
+
</td>
|
|
119
|
+
|
|
120
|
+
<td class="px-4 py-3 whitespace-nowrap cursor-pointer" onclick="window.location='<%= ruby_llm_agents.execution_path(execution) %>'">
|
|
91
121
|
<span class="text-sm font-medium text-gray-900 dark:text-gray-100">
|
|
92
122
|
<%= execution.agent_type.gsub(/Agent$/, '') %>
|
|
93
123
|
</span>
|
|
94
124
|
</td>
|
|
95
125
|
|
|
96
|
-
<td class="px-4 py-3 whitespace-nowrap">
|
|
126
|
+
<td class="px-4 py-3 whitespace-nowrap cursor-pointer" onclick="window.location='<%= ruby_llm_agents.execution_path(execution) %>'">
|
|
97
127
|
<%= render "rubyllm/agents/shared/status_badge", status: execution.status %>
|
|
98
128
|
</td>
|
|
99
129
|
|
|
100
|
-
<td class="px-4 py-3 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400">
|
|
130
|
+
<td class="px-4 py-3 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400 cursor-pointer" onclick="window.location='<%= ruby_llm_agents.execution_path(execution) %>'">
|
|
101
131
|
v<%= execution.agent_version %>
|
|
102
132
|
</td>
|
|
103
133
|
|
|
134
|
+
<!-- Attempts Count -->
|
|
135
|
+
<td class="px-4 py-3 whitespace-nowrap text-sm text-center cursor-pointer" onclick="window.location='<%= ruby_llm_agents.execution_path(execution) %>'">
|
|
136
|
+
<% attempts_count = execution.attempts_count || (execution.attempts&.size || 1) %>
|
|
137
|
+
<% if attempts_count > 1 %>
|
|
138
|
+
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200">
|
|
139
|
+
<%= attempts_count %> attempts
|
|
140
|
+
</span>
|
|
141
|
+
<% else %>
|
|
142
|
+
<span class="text-gray-400 dark:text-gray-500">1</span>
|
|
143
|
+
<% end %>
|
|
144
|
+
</td>
|
|
145
|
+
|
|
104
146
|
<td
|
|
105
147
|
class="
|
|
106
148
|
px-4 py-3 whitespace-nowrap text-sm text-gray-900 dark:text-gray-100
|
|
107
|
-
text-right font-medium
|
|
149
|
+
text-right font-medium cursor-pointer
|
|
108
150
|
"
|
|
151
|
+
onclick="window.location='<%= ruby_llm_agents.execution_path(execution) %>'"
|
|
109
152
|
>
|
|
110
153
|
<%= number_to_human_short(execution.total_tokens || 0) %>
|
|
111
154
|
</td>
|
|
@@ -113,8 +156,9 @@
|
|
|
113
156
|
<td
|
|
114
157
|
class="
|
|
115
158
|
px-4 py-3 whitespace-nowrap text-sm text-gray-900 dark:text-gray-100
|
|
116
|
-
text-right font-medium
|
|
159
|
+
text-right font-medium cursor-pointer
|
|
117
160
|
"
|
|
161
|
+
onclick="window.location='<%= ruby_llm_agents.execution_path(execution) %>'"
|
|
118
162
|
>
|
|
119
163
|
<%= number_to_human_short(execution.total_cost || 0, prefix: "$", precision: 2) %>
|
|
120
164
|
</td>
|
|
@@ -122,21 +166,68 @@
|
|
|
122
166
|
<td
|
|
123
167
|
class="
|
|
124
168
|
px-4 py-3 whitespace-nowrap text-sm text-gray-900 dark:text-gray-100
|
|
125
|
-
text-right font-medium
|
|
169
|
+
text-right font-medium cursor-pointer
|
|
126
170
|
"
|
|
171
|
+
onclick="window.location='<%= ruby_llm_agents.execution_path(execution) %>'"
|
|
127
172
|
>
|
|
128
173
|
<%= number_with_delimiter(execution.duration_ms || 0) %>ms
|
|
129
174
|
</td>
|
|
130
175
|
|
|
131
|
-
<td class="px-4 py-3 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400 text-right">
|
|
176
|
+
<td class="px-4 py-3 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400 text-right cursor-pointer" onclick="window.location='<%= ruby_llm_agents.execution_path(execution) %>'">
|
|
132
177
|
<%= time_ago_in_words(execution.created_at) %> ago
|
|
133
178
|
</td>
|
|
134
179
|
</tr>
|
|
135
180
|
|
|
181
|
+
<!-- Inline Attempts Expansion Row (hidden by default) -->
|
|
182
|
+
<% if has_attempts %>
|
|
183
|
+
<tr id="attempts-row-<%= execution.id %>" class="hidden bg-gray-50 dark:bg-gray-900">
|
|
184
|
+
<td colspan="9" class="px-4 py-3">
|
|
185
|
+
<div class="ml-6">
|
|
186
|
+
<h4 class="text-xs font-medium text-gray-500 dark:text-gray-400 uppercase mb-2">Attempts Timeline</h4>
|
|
187
|
+
<div class="space-y-2">
|
|
188
|
+
<% execution.attempts.each_with_index do |attempt, idx| %>
|
|
189
|
+
<% is_successful = attempt["error_class"].nil? && !attempt["short_circuited"] %>
|
|
190
|
+
<% is_short_circuited = attempt["short_circuited"] %>
|
|
191
|
+
<div class="flex items-center gap-3 text-sm <%= is_successful ? 'bg-green-50 dark:bg-green-900/20 border-l-2 border-green-500' : is_short_circuited ? 'bg-gray-100 dark:bg-gray-800 border-l-2 border-gray-400' : 'bg-red-50 dark:bg-red-900/20 border-l-2 border-red-500' %> px-3 py-2 rounded-r">
|
|
192
|
+
<span class="font-medium text-gray-700 dark:text-gray-300 w-6">#<%= idx + 1 %></span>
|
|
193
|
+
<span class="text-gray-600 dark:text-gray-400 w-32 truncate" title="<%= attempt["model_id"] %>">
|
|
194
|
+
<%= attempt["model_id"]&.split("/")&.last || "unknown" %>
|
|
195
|
+
</span>
|
|
196
|
+
<% if is_successful %>
|
|
197
|
+
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-green-100 text-green-800 dark:bg-green-800 dark:text-green-200">
|
|
198
|
+
Success
|
|
199
|
+
</span>
|
|
200
|
+
<% elsif is_short_circuited %>
|
|
201
|
+
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-gray-200 text-gray-600 dark:bg-gray-700 dark:text-gray-400">
|
|
202
|
+
Skipped (breaker)
|
|
203
|
+
</span>
|
|
204
|
+
<% else %>
|
|
205
|
+
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-red-100 text-red-800 dark:bg-red-800 dark:text-red-200" title="<%= attempt["error_message"] %>">
|
|
206
|
+
<%= attempt["error_class"]&.split("::")&.last || "Error" %>
|
|
207
|
+
</span>
|
|
208
|
+
<% end %>
|
|
209
|
+
<% if attempt["duration_ms"] %>
|
|
210
|
+
<span class="text-gray-500 dark:text-gray-400 text-xs">
|
|
211
|
+
<%= number_with_delimiter(attempt["duration_ms"].to_i) %>ms
|
|
212
|
+
</span>
|
|
213
|
+
<% end %>
|
|
214
|
+
<% if attempt["input_tokens"] || attempt["output_tokens"] %>
|
|
215
|
+
<span class="text-gray-500 dark:text-gray-400 text-xs">
|
|
216
|
+
<%= number_to_human_short((attempt["input_tokens"] || 0) + (attempt["output_tokens"] || 0)) %> tokens
|
|
217
|
+
</span>
|
|
218
|
+
<% end %>
|
|
219
|
+
</div>
|
|
220
|
+
<% end %>
|
|
221
|
+
</div>
|
|
222
|
+
</div>
|
|
223
|
+
</td>
|
|
224
|
+
</tr>
|
|
225
|
+
<% end %>
|
|
226
|
+
|
|
136
227
|
<% if execution.status_error? && execution.error_message.present? %>
|
|
137
228
|
<tr class="bg-red-50 dark:bg-red-900/30">
|
|
138
|
-
<td colspan="
|
|
139
|
-
<p class="text-xs text-red-600 dark:text-red-400">
|
|
229
|
+
<td colspan="9" class="px-4 py-2">
|
|
230
|
+
<p class="text-xs text-red-600 dark:text-red-400 ml-8">
|
|
140
231
|
<span class="font-medium">
|
|
141
232
|
<%= execution.error_class %>:
|
|
142
233
|
</span>
|