ruby_llm-agents 0.2.3 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +273 -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 +139 -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 +580 -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 +59 -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
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
<%# Collapsible version comparison UI - simplified for quick scanning %>
|
|
2
|
+
<% if versions.size >= 2 %>
|
|
3
|
+
<%
|
|
4
|
+
v1 = version_comparison&.dig(:v1)
|
|
5
|
+
v2 = version_comparison&.dig(:v2)
|
|
6
|
+
data = version_comparison&.dig(:data) || {}
|
|
7
|
+
v1_stats = data[:v1] || {}
|
|
8
|
+
v2_stats = data[:v2] || {}
|
|
9
|
+
|
|
10
|
+
# Calculate metrics with changes
|
|
11
|
+
metrics = [
|
|
12
|
+
{ name: "Success Rate", v1: v1_stats[:success_rate] || 0, v2: v2_stats[:success_rate] || 0, format: :pct, better: :higher },
|
|
13
|
+
{ name: "Avg Cost", v1: v1_stats[:avg_cost] || 0, v2: v2_stats[:avg_cost] || 0, format: :cost, better: :lower },
|
|
14
|
+
{ name: "Avg Tokens", v1: v1_stats[:avg_tokens] || 0, v2: v2_stats[:avg_tokens] || 0, format: :num, better: :lower },
|
|
15
|
+
{ name: "Avg Duration", v1: v1_stats[:avg_duration_ms] || 0, v2: v2_stats[:avg_duration_ms] || 0, format: :ms, better: :lower },
|
|
16
|
+
{ name: "Executions", v1: v1_stats[:count] || 0, v2: v2_stats[:count] || 0, format: :num, better: :higher }
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
# Count improvements/regressions for summary
|
|
20
|
+
improvements = 0
|
|
21
|
+
regressions = 0
|
|
22
|
+
metrics.each do |m|
|
|
23
|
+
next if m[:v1].zero? && m[:v2].zero?
|
|
24
|
+
if m[:better] == :higher
|
|
25
|
+
improvements += 1 if m[:v2] > m[:v1]
|
|
26
|
+
regressions += 1 if m[:v2] < m[:v1]
|
|
27
|
+
else
|
|
28
|
+
improvements += 1 if m[:v2] < m[:v1]
|
|
29
|
+
regressions += 1 if m[:v2] > m[:v1]
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
%>
|
|
33
|
+
|
|
34
|
+
<div class="bg-white dark:bg-gray-800 rounded-lg shadow mb-6">
|
|
35
|
+
<!-- Collapsible Header -->
|
|
36
|
+
<button
|
|
37
|
+
type="button"
|
|
38
|
+
onclick="toggleVersionComparison()"
|
|
39
|
+
class="w-full px-6 py-4 flex items-center justify-between hover:bg-gray-50 dark:hover:bg-gray-700/50 transition-colors rounded-lg"
|
|
40
|
+
>
|
|
41
|
+
<div class="flex items-center gap-3">
|
|
42
|
+
<svg id="version-chevron" class="w-5 h-5 text-gray-400 transition-transform duration-200" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
43
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/>
|
|
44
|
+
</svg>
|
|
45
|
+
<h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100">Version Comparison</h3>
|
|
46
|
+
<span class="text-sm text-gray-500 dark:text-gray-400">v<%= v1 %> → v<%= v2 %></span>
|
|
47
|
+
</div>
|
|
48
|
+
|
|
49
|
+
<!-- Quick Summary Pills -->
|
|
50
|
+
<div class="flex items-center gap-2">
|
|
51
|
+
<% if improvements > 0 %>
|
|
52
|
+
<span class="px-2 py-0.5 text-xs font-medium text-green-700 dark:text-green-300 bg-green-100 dark:bg-green-900/50 rounded-full">
|
|
53
|
+
<%= improvements %> improved
|
|
54
|
+
</span>
|
|
55
|
+
<% end %>
|
|
56
|
+
<% if regressions > 0 %>
|
|
57
|
+
<span class="px-2 py-0.5 text-xs font-medium text-red-700 dark:text-red-300 bg-red-100 dark:bg-red-900/50 rounded-full">
|
|
58
|
+
<%= regressions %> regressed
|
|
59
|
+
</span>
|
|
60
|
+
<% end %>
|
|
61
|
+
<% if improvements == 0 && regressions == 0 %>
|
|
62
|
+
<span class="px-2 py-0.5 text-xs font-medium text-gray-600 dark:text-gray-400 bg-gray-100 dark:bg-gray-700 rounded-full">
|
|
63
|
+
No changes
|
|
64
|
+
</span>
|
|
65
|
+
<% end %>
|
|
66
|
+
</div>
|
|
67
|
+
</button>
|
|
68
|
+
|
|
69
|
+
<!-- Collapsible Content -->
|
|
70
|
+
<div id="version-comparison-content" class="hidden border-t border-gray-100 dark:border-gray-700">
|
|
71
|
+
<div class="p-6">
|
|
72
|
+
<!-- Version Selectors -->
|
|
73
|
+
<div class="flex items-center gap-4 mb-4">
|
|
74
|
+
<select id="compare-v1" onchange="updateVersionComparison()" class="flex-1 px-3 py-2 text-sm bg-gray-50 dark:bg-gray-700 border border-gray-200 dark:border-gray-600 rounded-lg dark:text-gray-200">
|
|
75
|
+
<% versions.each do |ver| %>
|
|
76
|
+
<option value="<%= ver %>" <%= ver == v1 ? 'selected' : '' %>>v<%= ver %></option>
|
|
77
|
+
<% end %>
|
|
78
|
+
</select>
|
|
79
|
+
<span class="text-gray-400">→</span>
|
|
80
|
+
<select id="compare-v2" onchange="updateVersionComparison()" class="flex-1 px-3 py-2 text-sm bg-gray-50 dark:bg-gray-700 border border-gray-200 dark:border-gray-600 rounded-lg dark:text-gray-200">
|
|
81
|
+
<% versions.each do |ver| %>
|
|
82
|
+
<option value="<%= ver %>" <%= ver == v2 ? 'selected' : '' %>>v<%= ver %></option>
|
|
83
|
+
<% end %>
|
|
84
|
+
</select>
|
|
85
|
+
</div>
|
|
86
|
+
|
|
87
|
+
<% if version_comparison && version_comparison[:data] %>
|
|
88
|
+
<!-- Simple Comparison Table -->
|
|
89
|
+
<table class="w-full text-sm">
|
|
90
|
+
<thead>
|
|
91
|
+
<tr class="text-xs text-gray-500 dark:text-gray-400 uppercase">
|
|
92
|
+
<th class="text-left py-2">Metric</th>
|
|
93
|
+
<th class="text-right py-2">v<%= v1 %></th>
|
|
94
|
+
<th class="text-right py-2">v<%= v2 %></th>
|
|
95
|
+
<th class="text-right py-2">Change</th>
|
|
96
|
+
</tr>
|
|
97
|
+
</thead>
|
|
98
|
+
<tbody class="divide-y divide-gray-100 dark:divide-gray-700">
|
|
99
|
+
<% metrics.each do |m| %>
|
|
100
|
+
<%
|
|
101
|
+
# Format values
|
|
102
|
+
case m[:format]
|
|
103
|
+
when :pct
|
|
104
|
+
f_v1 = "#{m[:v1].round(1)}%"
|
|
105
|
+
f_v2 = "#{m[:v2].round(1)}%"
|
|
106
|
+
diff = m[:v2] - m[:v1]
|
|
107
|
+
change = "#{diff >= 0 ? '+' : ''}#{diff.round(1)}pp"
|
|
108
|
+
when :cost
|
|
109
|
+
f_v1 = "$#{number_with_precision(m[:v1], precision: 4)}"
|
|
110
|
+
f_v2 = "$#{number_with_precision(m[:v2], precision: 4)}"
|
|
111
|
+
pct = m[:v1] > 0 ? ((m[:v2] - m[:v1]) / m[:v1] * 100) : 0
|
|
112
|
+
change = "#{pct >= 0 ? '+' : ''}#{pct.round(1)}%"
|
|
113
|
+
when :ms
|
|
114
|
+
f_v1 = "#{number_with_delimiter(m[:v1].round)}ms"
|
|
115
|
+
f_v2 = "#{number_with_delimiter(m[:v2].round)}ms"
|
|
116
|
+
pct = m[:v1] > 0 ? ((m[:v2] - m[:v1]) / m[:v1] * 100) : 0
|
|
117
|
+
change = "#{pct >= 0 ? '+' : ''}#{pct.round(1)}%"
|
|
118
|
+
else
|
|
119
|
+
f_v1 = number_with_delimiter(m[:v1].round)
|
|
120
|
+
f_v2 = number_with_delimiter(m[:v2].round)
|
|
121
|
+
pct = m[:v1] > 0 ? ((m[:v2] - m[:v1]) / m[:v1] * 100) : 0
|
|
122
|
+
change = "#{pct >= 0 ? '+' : ''}#{pct.round(1)}%"
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# Determine if change is good/bad
|
|
126
|
+
is_better = m[:better] == :higher ? m[:v2] > m[:v1] : m[:v2] < m[:v1]
|
|
127
|
+
is_worse = m[:better] == :higher ? m[:v2] < m[:v1] : m[:v2] > m[:v1]
|
|
128
|
+
change_class = is_better ? "text-green-600 dark:text-green-400" : is_worse ? "text-red-600 dark:text-red-400" : "text-gray-500"
|
|
129
|
+
%>
|
|
130
|
+
<tr>
|
|
131
|
+
<td class="py-2 text-gray-700 dark:text-gray-300"><%= m[:name] %></td>
|
|
132
|
+
<td class="py-2 text-right text-gray-900 dark:text-gray-100 font-medium"><%= f_v1 %></td>
|
|
133
|
+
<td class="py-2 text-right text-gray-900 dark:text-gray-100 font-medium"><%= f_v2 %></td>
|
|
134
|
+
<td class="py-2 text-right font-medium <%= change_class %>"><%= change %></td>
|
|
135
|
+
</tr>
|
|
136
|
+
<% end %>
|
|
137
|
+
</tbody>
|
|
138
|
+
</table>
|
|
139
|
+
|
|
140
|
+
<p class="mt-4 text-xs text-gray-400 dark:text-gray-500 text-center">
|
|
141
|
+
Based on this month's data
|
|
142
|
+
</p>
|
|
143
|
+
<% else %>
|
|
144
|
+
<p class="text-sm text-gray-500 dark:text-gray-400 text-center py-4">
|
|
145
|
+
No data available for selected versions
|
|
146
|
+
</p>
|
|
147
|
+
<% end %>
|
|
148
|
+
</div>
|
|
149
|
+
</div>
|
|
150
|
+
</div>
|
|
151
|
+
|
|
152
|
+
<script>
|
|
153
|
+
function toggleVersionComparison() {
|
|
154
|
+
const content = document.getElementById('version-comparison-content');
|
|
155
|
+
const chevron = document.getElementById('version-chevron');
|
|
156
|
+
content.classList.toggle('hidden');
|
|
157
|
+
chevron.classList.toggle('-rotate-180');
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function updateVersionComparison() {
|
|
161
|
+
const v1 = document.getElementById('compare-v1').value;
|
|
162
|
+
const v2 = document.getElementById('compare-v2').value;
|
|
163
|
+
const url = new URL(window.location.href);
|
|
164
|
+
url.searchParams.set('compare_v1', v1);
|
|
165
|
+
url.searchParams.set('compare_v2', v2);
|
|
166
|
+
window.location.href = url.toString();
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Restore collapsed state from localStorage
|
|
170
|
+
document.addEventListener('DOMContentLoaded', function() {
|
|
171
|
+
const expanded = localStorage.getItem('version_comparison_expanded');
|
|
172
|
+
if (expanded === 'true') {
|
|
173
|
+
document.getElementById('version-comparison-content').classList.remove('hidden');
|
|
174
|
+
document.getElementById('version-chevron').classList.add('-rotate-180');
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
// Save state when toggling
|
|
179
|
+
const originalToggle = toggleVersionComparison;
|
|
180
|
+
toggleVersionComparison = function() {
|
|
181
|
+
originalToggle();
|
|
182
|
+
const isExpanded = !document.getElementById('version-comparison-content').classList.contains('hidden');
|
|
183
|
+
localStorage.setItem('version_comparison_expanded', isExpanded);
|
|
184
|
+
};
|
|
185
|
+
</script>
|
|
186
|
+
<% end %>
|
|
@@ -67,10 +67,58 @@
|
|
|
67
67
|
</div>
|
|
68
68
|
</div>
|
|
69
69
|
|
|
70
|
+
<!-- Circuit Breaker Status (if reliability enabled) -->
|
|
71
|
+
<% if @config && @circuit_breaker_status.present? %>
|
|
72
|
+
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-4 mb-6">
|
|
73
|
+
<div class="flex items-center justify-between mb-3">
|
|
74
|
+
<h3 class="text-sm font-medium text-gray-700 dark:text-gray-300 uppercase tracking-wider">Circuit Breaker Status</h3>
|
|
75
|
+
<span class="text-xs text-gray-400 dark:text-gray-500">Auto-refreshes every 30s</span>
|
|
76
|
+
</div>
|
|
77
|
+
|
|
78
|
+
<div class="flex flex-wrap gap-3">
|
|
79
|
+
<% @circuit_breaker_status.each do |model_id, status| %>
|
|
80
|
+
<div class="flex items-center gap-2 px-3 py-2 rounded-lg border <%= status[:open] ? 'bg-red-50 dark:bg-red-900/20 border-red-200 dark:border-red-800' : 'bg-green-50 dark:bg-green-900/20 border-green-200 dark:border-green-800' %>">
|
|
81
|
+
<% if status[:open] %>
|
|
82
|
+
<!-- Open/Tripped indicator -->
|
|
83
|
+
<span class="relative flex h-3 w-3">
|
|
84
|
+
<span class="animate-ping absolute inline-flex h-full w-full rounded-full bg-red-400 opacity-75"></span>
|
|
85
|
+
<span class="relative inline-flex rounded-full h-3 w-3 bg-red-500"></span>
|
|
86
|
+
</span>
|
|
87
|
+
<span class="text-sm font-medium text-red-700 dark:text-red-300"><%= model_id %></span>
|
|
88
|
+
<span class="text-xs text-red-500 dark:text-red-400 ml-1">OPEN</span>
|
|
89
|
+
<% if status[:cooldown_remaining] %>
|
|
90
|
+
<span class="text-xs text-red-400 dark:text-red-500 ml-1">
|
|
91
|
+
(resets in <%= status[:cooldown_remaining] %>s)
|
|
92
|
+
</span>
|
|
93
|
+
<% end %>
|
|
94
|
+
<% else %>
|
|
95
|
+
<!-- Closed/Healthy indicator -->
|
|
96
|
+
<span class="inline-flex rounded-full h-3 w-3 bg-green-500"></span>
|
|
97
|
+
<span class="text-sm font-medium text-green-700 dark:text-green-300"><%= model_id %></span>
|
|
98
|
+
<span class="text-xs text-green-500 dark:text-green-400 ml-1">OK</span>
|
|
99
|
+
<% if status[:failure_count] && status[:failure_count] > 0 %>
|
|
100
|
+
<span class="text-xs text-yellow-500 dark:text-yellow-400 ml-1">
|
|
101
|
+
(<%= status[:failure_count] %>/<%= status[:threshold] %> failures)
|
|
102
|
+
</span>
|
|
103
|
+
<% end %>
|
|
104
|
+
<% end %>
|
|
105
|
+
</div>
|
|
106
|
+
<% end %>
|
|
107
|
+
</div>
|
|
108
|
+
|
|
109
|
+
<% if @circuit_breaker_status.values.any? { |s| s[:open] } %>
|
|
110
|
+
<p class="mt-3 text-xs text-gray-500 dark:text-gray-400">
|
|
111
|
+
Open circuit breakers will skip the model and try fallbacks (if configured).
|
|
112
|
+
They automatically reset after the cooldown period.
|
|
113
|
+
</p>
|
|
114
|
+
<% end %>
|
|
115
|
+
</div>
|
|
116
|
+
<% end %>
|
|
117
|
+
|
|
70
118
|
<!-- Stats Grid -->
|
|
71
119
|
<% success_rate = @stats[:success_rate] || 0 %>
|
|
72
120
|
<% success_rate_color = success_rate >= 95 ? 'text-green-600' : success_rate >= 80 ? 'text-yellow-600' : 'text-red-600' %>
|
|
73
|
-
<div class="grid grid-cols-2 md:grid-cols-
|
|
121
|
+
<div class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-6 gap-4 mb-6">
|
|
74
122
|
<%= render "rubyllm/agents/shared/stat_card",
|
|
75
123
|
title: "Executions",
|
|
76
124
|
value: number_with_delimiter(@stats[:count]),
|
|
@@ -105,6 +153,12 @@
|
|
|
105
153
|
value: "#{number_with_delimiter(@stats[:avg_duration_ms] || 0)} ms",
|
|
106
154
|
icon: "M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z",
|
|
107
155
|
icon_color: "text-purple-500" %>
|
|
156
|
+
|
|
157
|
+
<%= render "rubyllm/agents/shared/stat_card",
|
|
158
|
+
title: "Cache Hit Rate",
|
|
159
|
+
value: "#{@cache_hit_rate}%",
|
|
160
|
+
icon: "M4 7v10c0 2.21 3.582 4 8 4s8-1.79 8-4V7M4 7c0 2.21 3.582 4 8 4s8-1.79 8-4M4 7c0-2.21 3.582-4 8-4s8 1.79 8 4",
|
|
161
|
+
icon_color: "text-purple-500" %>
|
|
108
162
|
</div>
|
|
109
163
|
|
|
110
164
|
<!-- Charts Section -->
|
|
@@ -122,7 +176,11 @@
|
|
|
122
176
|
<%= area_chart [
|
|
123
177
|
{ name: "Success", data: success_data },
|
|
124
178
|
{ name: "Failed", data: failed_data }
|
|
125
|
-
], colors: ["#10B981", "#EF4444"], stacked: true, library: {
|
|
179
|
+
], colors: ["#10B981", "#EF4444"], stacked: true, library: {
|
|
180
|
+
yAxis: { min: 0 },
|
|
181
|
+
legend: { align: "center", verticalAlign: "bottom" },
|
|
182
|
+
plotOptions: { area: { stacking: "normal" } }
|
|
183
|
+
} %>
|
|
126
184
|
</div>
|
|
127
185
|
</div>
|
|
128
186
|
|
|
@@ -133,7 +191,10 @@
|
|
|
133
191
|
<div id="cost-chart" style="height: 250px;">
|
|
134
192
|
<% cost_data = @trend_data.map { |d| [d[:date].strftime("%b %d"), d[:total_cost].to_f.round(4)] }.to_h %>
|
|
135
193
|
|
|
136
|
-
<%= line_chart cost_data, colors: ["#10B981"], prefix: "$", library: {
|
|
194
|
+
<%= line_chart cost_data, colors: ["#10B981"], prefix: "$", library: {
|
|
195
|
+
yAxis: { min: 0 },
|
|
196
|
+
legend: { enabled: false }
|
|
197
|
+
} %>
|
|
137
198
|
</div>
|
|
138
199
|
</div>
|
|
139
200
|
</div>
|
|
@@ -169,12 +230,52 @@
|
|
|
169
230
|
</div>
|
|
170
231
|
</div>
|
|
171
232
|
|
|
233
|
+
<!-- Finish Reason Distribution -->
|
|
234
|
+
<% if @finish_reason_distribution.present? && @finish_reason_distribution.any? %>
|
|
235
|
+
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-4 mb-6">
|
|
236
|
+
<div class="flex items-center justify-between">
|
|
237
|
+
<p class="text-sm text-gray-500 dark:text-gray-400 uppercase">Finish Reasons</p>
|
|
238
|
+
|
|
239
|
+
<div class="flex flex-wrap gap-4">
|
|
240
|
+
<% finish_colors = {
|
|
241
|
+
'stop' => '#10B981',
|
|
242
|
+
'length' => '#F59E0B',
|
|
243
|
+
'content_filter' => '#EF4444',
|
|
244
|
+
'tool_calls' => '#3B82F6',
|
|
245
|
+
nil => '#6B7280'
|
|
246
|
+
} %>
|
|
247
|
+
|
|
248
|
+
<% @finish_reason_distribution.each do |reason, count| %>
|
|
249
|
+
<div class="flex items-center">
|
|
250
|
+
<span
|
|
251
|
+
class="w-2 h-2 rounded-full mr-1.5"
|
|
252
|
+
style="background-color: <%= finish_colors[reason] || '#6B7280' %>"
|
|
253
|
+
></span>
|
|
254
|
+
|
|
255
|
+
<span class="text-sm text-gray-700 dark:text-gray-300"><%= reason || 'unknown' %></span>
|
|
256
|
+
|
|
257
|
+
<span class="text-sm font-medium text-gray-900 dark:text-gray-100 ml-1">
|
|
258
|
+
(<%= number_with_delimiter(count) %>)
|
|
259
|
+
</span>
|
|
260
|
+
</div>
|
|
261
|
+
<% end %>
|
|
262
|
+
</div>
|
|
263
|
+
</div>
|
|
264
|
+
</div>
|
|
265
|
+
<% end %>
|
|
266
|
+
|
|
267
|
+
<!-- Version Comparison -->
|
|
268
|
+
<%= render partial: "rubyllm/agents/agents/version_comparison",
|
|
269
|
+
locals: { versions: @versions, version_comparison: @version_comparison } %>
|
|
270
|
+
|
|
172
271
|
<% if @config %>
|
|
173
272
|
<!-- Configuration -->
|
|
174
273
|
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6 mb-6">
|
|
175
274
|
<h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-4">Configuration</h3>
|
|
176
275
|
|
|
177
|
-
|
|
276
|
+
<!-- Basic Configuration -->
|
|
277
|
+
<p class="text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wider mb-3">Basic</p>
|
|
278
|
+
<div class="grid grid-cols-2 md:grid-cols-4 gap-4 mb-6">
|
|
178
279
|
<div>
|
|
179
280
|
<p class="text-sm text-gray-500 dark:text-gray-400">Model</p>
|
|
180
281
|
<p class="font-medium text-gray-900 dark:text-gray-100"><%= @config[:model] %></p>
|
|
@@ -192,22 +293,144 @@
|
|
|
192
293
|
|
|
193
294
|
<div>
|
|
194
295
|
<p class="text-sm text-gray-500 dark:text-gray-400">Cache</p>
|
|
195
|
-
|
|
196
296
|
<p class="font-medium text-gray-900 dark:text-gray-100">
|
|
197
297
|
<% if @config[:cache_enabled] %>
|
|
198
|
-
Enabled (
|
|
199
|
-
<%= @config[:cache_ttl].inspect %>
|
|
200
|
-
)
|
|
298
|
+
Enabled (<%= @config[:cache_ttl].inspect %>)
|
|
201
299
|
<% else %>
|
|
202
|
-
Disabled
|
|
300
|
+
<span class="text-gray-400 dark:text-gray-500">Disabled</span>
|
|
203
301
|
<% end %>
|
|
204
302
|
</p>
|
|
205
303
|
</div>
|
|
206
304
|
</div>
|
|
207
305
|
|
|
306
|
+
<!-- Reliability Configuration -->
|
|
307
|
+
<%
|
|
308
|
+
retries_config = @config[:retries] || {}
|
|
309
|
+
has_retries = (retries_config[:max] || 0) > 0
|
|
310
|
+
has_fallbacks = @config[:fallback_models].present? && @config[:fallback_models].any?
|
|
311
|
+
has_total_timeout = @config[:total_timeout].present?
|
|
312
|
+
has_circuit_breaker = @config[:circuit_breaker].present?
|
|
313
|
+
has_any_reliability = has_retries || has_fallbacks || has_total_timeout || has_circuit_breaker
|
|
314
|
+
%>
|
|
315
|
+
<div class="border-t border-gray-100 dark:border-gray-700 pt-4 mb-6">
|
|
316
|
+
<p class="text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wider mb-3">Reliability</p>
|
|
317
|
+
|
|
318
|
+
<% if has_any_reliability %>
|
|
319
|
+
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
320
|
+
<!-- Retries -->
|
|
321
|
+
<div class="flex items-start gap-3 p-3 rounded-lg <%= has_retries ? 'bg-green-50 dark:bg-green-900/20' : 'bg-gray-50 dark:bg-gray-700/50' %>">
|
|
322
|
+
<div class="flex-shrink-0 mt-0.5">
|
|
323
|
+
<% if has_retries %>
|
|
324
|
+
<svg class="w-5 h-5 text-green-500" fill="currentColor" viewBox="0 0 20 20">
|
|
325
|
+
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd"/>
|
|
326
|
+
</svg>
|
|
327
|
+
<% else %>
|
|
328
|
+
<svg class="w-5 h-5 text-gray-400" fill="currentColor" viewBox="0 0 20 20">
|
|
329
|
+
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd"/>
|
|
330
|
+
</svg>
|
|
331
|
+
<% end %>
|
|
332
|
+
</div>
|
|
333
|
+
<div>
|
|
334
|
+
<p class="text-sm font-medium text-gray-900 dark:text-gray-100">Retries</p>
|
|
335
|
+
<% if has_retries %>
|
|
336
|
+
<p class="text-xs text-gray-600 dark:text-gray-400 mt-1">
|
|
337
|
+
Max: <%= retries_config[:max] %> ·
|
|
338
|
+
Backoff: <%= retries_config[:backoff] %> ·
|
|
339
|
+
Base: <%= retries_config[:base] %>s ·
|
|
340
|
+
Max delay: <%= retries_config[:max_delay] %>s
|
|
341
|
+
</p>
|
|
342
|
+
<% else %>
|
|
343
|
+
<p class="text-xs text-gray-400 dark:text-gray-500 mt-1">Not configured</p>
|
|
344
|
+
<% end %>
|
|
345
|
+
</div>
|
|
346
|
+
</div>
|
|
347
|
+
|
|
348
|
+
<!-- Fallback Models -->
|
|
349
|
+
<div class="flex items-start gap-3 p-3 rounded-lg <%= has_fallbacks ? 'bg-green-50 dark:bg-green-900/20' : 'bg-gray-50 dark:bg-gray-700/50' %>">
|
|
350
|
+
<div class="flex-shrink-0 mt-0.5">
|
|
351
|
+
<% if has_fallbacks %>
|
|
352
|
+
<svg class="w-5 h-5 text-green-500" fill="currentColor" viewBox="0 0 20 20">
|
|
353
|
+
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd"/>
|
|
354
|
+
</svg>
|
|
355
|
+
<% else %>
|
|
356
|
+
<svg class="w-5 h-5 text-gray-400" fill="currentColor" viewBox="0 0 20 20">
|
|
357
|
+
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd"/>
|
|
358
|
+
</svg>
|
|
359
|
+
<% end %>
|
|
360
|
+
</div>
|
|
361
|
+
<div>
|
|
362
|
+
<p class="text-sm font-medium text-gray-900 dark:text-gray-100">Fallback Models</p>
|
|
363
|
+
<% if has_fallbacks %>
|
|
364
|
+
<p class="text-xs text-gray-600 dark:text-gray-400 mt-1">
|
|
365
|
+
<%= @config[:fallback_models].join(" → ") %>
|
|
366
|
+
</p>
|
|
367
|
+
<% else %>
|
|
368
|
+
<p class="text-xs text-gray-400 dark:text-gray-500 mt-1">Not configured</p>
|
|
369
|
+
<% end %>
|
|
370
|
+
</div>
|
|
371
|
+
</div>
|
|
372
|
+
|
|
373
|
+
<!-- Total Timeout -->
|
|
374
|
+
<div class="flex items-start gap-3 p-3 rounded-lg <%= has_total_timeout ? 'bg-green-50 dark:bg-green-900/20' : 'bg-gray-50 dark:bg-gray-700/50' %>">
|
|
375
|
+
<div class="flex-shrink-0 mt-0.5">
|
|
376
|
+
<% if has_total_timeout %>
|
|
377
|
+
<svg class="w-5 h-5 text-green-500" fill="currentColor" viewBox="0 0 20 20">
|
|
378
|
+
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd"/>
|
|
379
|
+
</svg>
|
|
380
|
+
<% else %>
|
|
381
|
+
<svg class="w-5 h-5 text-gray-400" fill="currentColor" viewBox="0 0 20 20">
|
|
382
|
+
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd"/>
|
|
383
|
+
</svg>
|
|
384
|
+
<% end %>
|
|
385
|
+
</div>
|
|
386
|
+
<div>
|
|
387
|
+
<p class="text-sm font-medium text-gray-900 dark:text-gray-100">Total Timeout</p>
|
|
388
|
+
<% if has_total_timeout %>
|
|
389
|
+
<p class="text-xs text-gray-600 dark:text-gray-400 mt-1">
|
|
390
|
+
<%= @config[:total_timeout] %> seconds across all attempts
|
|
391
|
+
</p>
|
|
392
|
+
<% else %>
|
|
393
|
+
<p class="text-xs text-gray-400 dark:text-gray-500 mt-1">Not configured</p>
|
|
394
|
+
<% end %>
|
|
395
|
+
</div>
|
|
396
|
+
</div>
|
|
397
|
+
|
|
398
|
+
<!-- Circuit Breaker -->
|
|
399
|
+
<div class="flex items-start gap-3 p-3 rounded-lg <%= has_circuit_breaker ? 'bg-green-50 dark:bg-green-900/20' : 'bg-gray-50 dark:bg-gray-700/50' %>">
|
|
400
|
+
<div class="flex-shrink-0 mt-0.5">
|
|
401
|
+
<% if has_circuit_breaker %>
|
|
402
|
+
<svg class="w-5 h-5 text-green-500" fill="currentColor" viewBox="0 0 20 20">
|
|
403
|
+
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd"/>
|
|
404
|
+
</svg>
|
|
405
|
+
<% else %>
|
|
406
|
+
<svg class="w-5 h-5 text-gray-400" fill="currentColor" viewBox="0 0 20 20">
|
|
407
|
+
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd"/>
|
|
408
|
+
</svg>
|
|
409
|
+
<% end %>
|
|
410
|
+
</div>
|
|
411
|
+
<div>
|
|
412
|
+
<p class="text-sm font-medium text-gray-900 dark:text-gray-100">Circuit Breaker</p>
|
|
413
|
+
<% if has_circuit_breaker %>
|
|
414
|
+
<% cb = @config[:circuit_breaker] %>
|
|
415
|
+
<p class="text-xs text-gray-600 dark:text-gray-400 mt-1">
|
|
416
|
+
Opens after <%= cb[:errors] %> errors within <%= cb[:within] %>s ·
|
|
417
|
+
Cooldown: <%= cb[:cooldown] %>s
|
|
418
|
+
</p>
|
|
419
|
+
<% else %>
|
|
420
|
+
<p class="text-xs text-gray-400 dark:text-gray-500 mt-1">Not configured</p>
|
|
421
|
+
<% end %>
|
|
422
|
+
</div>
|
|
423
|
+
</div>
|
|
424
|
+
</div>
|
|
425
|
+
<% else %>
|
|
426
|
+
<p class="text-sm text-gray-400 dark:text-gray-500">No reliability features configured</p>
|
|
427
|
+
<% end %>
|
|
428
|
+
</div>
|
|
429
|
+
|
|
430
|
+
<!-- Parameters -->
|
|
208
431
|
<% if @config[:params].present? && @config[:params].any? %>
|
|
209
432
|
<div class="border-t border-gray-100 dark:border-gray-700 pt-4">
|
|
210
|
-
<p class="text-
|
|
433
|
+
<p class="text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wider mb-3">Parameters</p>
|
|
211
434
|
|
|
212
435
|
<div class="space-y-2">
|
|
213
436
|
<% @config[:params].each do |name, opts| %>
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
<% if critical_alerts.any? %>
|
|
2
|
+
<div id="action-center" class="mb-6 bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-xl p-4">
|
|
3
|
+
<div class="flex items-center mb-3">
|
|
4
|
+
<svg class="w-5 h-5 text-red-600 dark:text-red-400 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
5
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"/>
|
|
6
|
+
</svg>
|
|
7
|
+
<h3 class="font-semibold text-red-800 dark:text-red-200">Action Required</h3>
|
|
8
|
+
</div>
|
|
9
|
+
|
|
10
|
+
<div class="space-y-2">
|
|
11
|
+
<% critical_alerts.each do |alert| %>
|
|
12
|
+
<div class="flex items-center justify-between bg-white dark:bg-gray-800 rounded-lg px-4 py-3 shadow-sm">
|
|
13
|
+
<% case alert[:type] %>
|
|
14
|
+
<% when :breaker %>
|
|
15
|
+
<div class="flex items-center">
|
|
16
|
+
<span class="w-2 h-2 bg-orange-500 rounded-full mr-3"></span>
|
|
17
|
+
<div>
|
|
18
|
+
<p class="text-sm font-medium text-gray-900 dark:text-gray-100">
|
|
19
|
+
Circuit breaker open: <%= alert[:data][:agent_type].gsub(/Agent$/, '') %>
|
|
20
|
+
</p>
|
|
21
|
+
<p class="text-xs text-gray-500 dark:text-gray-400">
|
|
22
|
+
<%= alert[:data][:model_id] %> · <%= alert[:data][:failure_count] %>/<%= alert[:data][:threshold] %> failures
|
|
23
|
+
</p>
|
|
24
|
+
</div>
|
|
25
|
+
</div>
|
|
26
|
+
<span class="text-xs text-orange-600 dark:text-orange-400 font-medium">
|
|
27
|
+
<%= alert[:data][:cooldown_remaining] %>s remaining
|
|
28
|
+
</span>
|
|
29
|
+
|
|
30
|
+
<% when :budget_breach %>
|
|
31
|
+
<div class="flex items-center">
|
|
32
|
+
<span class="w-2 h-2 bg-red-500 rounded-full mr-3"></span>
|
|
33
|
+
<div>
|
|
34
|
+
<p class="text-sm font-medium text-gray-900 dark:text-gray-100">
|
|
35
|
+
<%= alert[:data][:period].to_s.capitalize %> budget exceeded
|
|
36
|
+
</p>
|
|
37
|
+
<p class="text-xs text-gray-500 dark:text-gray-400">
|
|
38
|
+
$<%= number_with_precision(alert[:data][:current], precision: 2) %> / $<%= number_with_precision(alert[:data][:limit], precision: 2) %>
|
|
39
|
+
</p>
|
|
40
|
+
</div>
|
|
41
|
+
</div>
|
|
42
|
+
<%= link_to "Adjust", ruby_llm_agents.settings_path, class: "text-xs text-red-600 dark:text-red-400 hover:underline font-medium" %>
|
|
43
|
+
|
|
44
|
+
<% when :error_spike %>
|
|
45
|
+
<div class="flex items-center">
|
|
46
|
+
<span class="w-2 h-2 bg-red-500 rounded-full mr-3 animate-pulse"></span>
|
|
47
|
+
<div>
|
|
48
|
+
<p class="text-sm font-medium text-gray-900 dark:text-gray-100">
|
|
49
|
+
Error spike detected
|
|
50
|
+
</p>
|
|
51
|
+
<p class="text-xs text-gray-500 dark:text-gray-400">
|
|
52
|
+
<%= alert[:data][:count] %> errors in last 15 minutes
|
|
53
|
+
</p>
|
|
54
|
+
</div>
|
|
55
|
+
</div>
|
|
56
|
+
<%= link_to "View failures", ruby_llm_agents.executions_path(status: "error"), class: "text-xs text-red-600 dark:text-red-400 hover:underline font-medium" %>
|
|
57
|
+
<% end %>
|
|
58
|
+
</div>
|
|
59
|
+
<% end %>
|
|
60
|
+
</div>
|
|
61
|
+
</div>
|
|
62
|
+
<% end %>
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
<%# Alerts feed for budget and breaker events %>
|
|
2
|
+
<% if recent_alerts.any? %>
|
|
3
|
+
<div class="bg-white dark:bg-gray-800 rounded-xl shadow-sm border border-gray-100 dark:border-gray-700 p-4 mb-6">
|
|
4
|
+
<div class="flex items-center justify-between mb-3">
|
|
5
|
+
<h3 class="text-sm font-semibold text-gray-700 dark:text-gray-200">Recent Alerts</h3>
|
|
6
|
+
<span class="text-xs text-gray-500 dark:text-gray-400">
|
|
7
|
+
Last <%= recent_alerts.size %> events
|
|
8
|
+
</span>
|
|
9
|
+
</div>
|
|
10
|
+
|
|
11
|
+
<div class="space-y-2 max-h-48 overflow-y-auto">
|
|
12
|
+
<% recent_alerts.each do |alert| %>
|
|
13
|
+
<%
|
|
14
|
+
icon_color = case alert[:type].to_s
|
|
15
|
+
when /budget/ then "text-amber-500"
|
|
16
|
+
when /breaker_open/ then "text-red-500"
|
|
17
|
+
when /breaker_closed/ then "text-green-500"
|
|
18
|
+
else "text-blue-500"
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
bg_color = case alert[:type].to_s
|
|
22
|
+
when /budget/ then "bg-amber-50 dark:bg-amber-900/20"
|
|
23
|
+
when /breaker_open/ then "bg-red-50 dark:bg-red-900/20"
|
|
24
|
+
when /breaker_closed/ then "bg-green-50 dark:bg-green-900/20"
|
|
25
|
+
else "bg-blue-50 dark:bg-blue-900/20"
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
icon_path = case alert[:type].to_s
|
|
29
|
+
when /budget/
|
|
30
|
+
"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"
|
|
31
|
+
when /breaker/
|
|
32
|
+
"M13 10V3L4 14h7v7l9-11h-7z"
|
|
33
|
+
else
|
|
34
|
+
"M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9"
|
|
35
|
+
end
|
|
36
|
+
%>
|
|
37
|
+
<div class="flex items-start gap-3 p-2 rounded-lg <%= bg_color %>">
|
|
38
|
+
<svg class="w-4 h-4 mt-0.5 <%= icon_color %> flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
39
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="<%= icon_path %>"/>
|
|
40
|
+
</svg>
|
|
41
|
+
<div class="flex-1 min-w-0">
|
|
42
|
+
<p class="text-sm text-gray-700 dark:text-gray-300">
|
|
43
|
+
<%= alert[:message] || alert[:type].to_s.humanize %>
|
|
44
|
+
</p>
|
|
45
|
+
<div class="flex items-center gap-2 mt-0.5">
|
|
46
|
+
<% if alert[:agent_type] %>
|
|
47
|
+
<span class="text-xs text-gray-500 dark:text-gray-400">
|
|
48
|
+
<%= alert[:agent_type].gsub(/Agent$/, '') %>
|
|
49
|
+
</span>
|
|
50
|
+
<% end %>
|
|
51
|
+
<% if alert[:timestamp] %>
|
|
52
|
+
<span class="text-xs text-gray-400 dark:text-gray-500">
|
|
53
|
+
<%= alert[:timestamp].is_a?(Time) ? time_ago_in_words(alert[:timestamp]) + " ago" : alert[:timestamp] %>
|
|
54
|
+
</span>
|
|
55
|
+
<% end %>
|
|
56
|
+
</div>
|
|
57
|
+
</div>
|
|
58
|
+
</div>
|
|
59
|
+
<% end %>
|
|
60
|
+
</div>
|
|
61
|
+
</div>
|
|
62
|
+
<% end %>
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
<%# Circuit breaker status strip %>
|
|
2
|
+
<div class="bg-white dark:bg-gray-800 rounded-xl shadow-sm border border-gray-100 dark:border-gray-700 p-4 mb-6">
|
|
3
|
+
<div class="flex items-center justify-between mb-3">
|
|
4
|
+
<h3 class="text-sm font-semibold text-gray-700 dark:text-gray-200">Circuit Breakers</h3>
|
|
5
|
+
<% if open_breakers.any? %>
|
|
6
|
+
<span class="flex items-center text-xs text-red-600 dark:text-red-400">
|
|
7
|
+
<span class="w-2 h-2 bg-red-500 rounded-full mr-1 animate-pulse"></span>
|
|
8
|
+
<%= open_breakers.size %> open
|
|
9
|
+
</span>
|
|
10
|
+
<% else %>
|
|
11
|
+
<span class="flex items-center text-xs text-green-600 dark:text-green-400">
|
|
12
|
+
<span class="w-2 h-2 bg-green-500 rounded-full mr-1"></span>
|
|
13
|
+
All healthy
|
|
14
|
+
</span>
|
|
15
|
+
<% end %>
|
|
16
|
+
</div>
|
|
17
|
+
|
|
18
|
+
<% if open_breakers.any? %>
|
|
19
|
+
<div class="flex flex-wrap gap-2">
|
|
20
|
+
<% open_breakers.each do |breaker| %>
|
|
21
|
+
<%= link_to ruby_llm_agents.agent_path(breaker[:agent_type]),
|
|
22
|
+
class: "inline-flex items-center px-3 py-1.5 bg-red-50 dark:bg-red-900/30 border border-red-200 dark:border-red-800 rounded-full text-sm hover:bg-red-100 dark:hover:bg-red-900/50 transition-colors" do %>
|
|
23
|
+
<span class="w-2 h-2 bg-red-500 rounded-full mr-2 animate-pulse"></span>
|
|
24
|
+
<span class="text-red-700 dark:text-red-300 font-medium">
|
|
25
|
+
<%= breaker[:agent_type].gsub(/Agent$/, '') %>
|
|
26
|
+
</span>
|
|
27
|
+
<span class="text-red-500 dark:text-red-400 mx-1">/</span>
|
|
28
|
+
<span class="text-red-600 dark:text-red-300 text-xs">
|
|
29
|
+
<%= breaker[:model_id].split('/').last %>
|
|
30
|
+
</span>
|
|
31
|
+
<% if breaker[:cooldown_remaining] && breaker[:cooldown_remaining] > 0 %>
|
|
32
|
+
<span class="ml-2 text-xs text-red-500 dark:text-red-400 tabular-nums" data-cooldown="<%= breaker[:cooldown_remaining] %>">
|
|
33
|
+
<%= breaker[:cooldown_remaining] %>s
|
|
34
|
+
</span>
|
|
35
|
+
<% end %>
|
|
36
|
+
<% end %>
|
|
37
|
+
<% end %>
|
|
38
|
+
</div>
|
|
39
|
+
<% else %>
|
|
40
|
+
<div class="flex items-center justify-center py-2 text-gray-500 dark:text-gray-400">
|
|
41
|
+
<svg class="w-4 h-4 mr-2 text-green-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
42
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
|
43
|
+
</svg>
|
|
44
|
+
<span class="text-sm">All circuit breakers are closed</span>
|
|
45
|
+
</div>
|
|
46
|
+
<% end %>
|
|
47
|
+
</div>
|