actionagent 1.9.3 → 1.9.4
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/app/assets/builds/action_agent.css +1 -1
- data/app/assets/builds/action_agent.js +60 -60
- data/app/controllers/action_agent/dashboard_controller.rb +1 -0
- data/app/controllers/action_agent/traces_controller.rb +6 -0
- data/app/views/action_agent/traces/_styles.html.erb +45 -0
- data/app/views/action_agent/traces/_summary.html.erb +8 -0
- data/app/views/action_agent/traces/_trace_detail.html.erb +33 -96
- data/app/views/action_agent/traces/index.html.erb +25 -134
- data/app/views/action_agent/traces/metrics.html.erb +22 -140
- data/app/views/action_agent/traces/show.html.erb +7 -36
- data/app/views/layouts/action_agent/application.html.erb +26 -86
- data/lib/action_agent/version.rb +1 -1
- data/lib/action_agent.rb +32 -0
- data/lib/generators/action_agent/templates/action_agent.rb.erb +4 -3
- metadata +4 -2
|
@@ -58,6 +58,7 @@ module ActionAgent
|
|
|
58
58
|
providerKeyScope: ActionAgent.provider_key_scope,
|
|
59
59
|
personalProviderKeys: ProviderKey.personal_keys_enabled?,
|
|
60
60
|
signOutPath: ActionAgent.sign_out_path,
|
|
61
|
+
userMenuLinks: ActionAgent.user_menu_links_for(current_user, current_owner),
|
|
61
62
|
# Named here rather than by the layout, so a host layout
|
|
62
63
|
# (ActionAgent.layout) does not have to know about it. Nil when the
|
|
63
64
|
# host turned capture off, and the dashboard then loads no recorder.
|
|
@@ -9,6 +9,8 @@ module ActionAgent
|
|
|
9
9
|
# - Metrics overview
|
|
10
10
|
# - Live updates via Turbo Streams
|
|
11
11
|
class TracesController < ApplicationController
|
|
12
|
+
before_action :set_refresh_interval
|
|
13
|
+
|
|
12
14
|
def index
|
|
13
15
|
@traces = fetch_traces
|
|
14
16
|
@metrics = calculate_metrics
|
|
@@ -38,6 +40,10 @@ module ActionAgent
|
|
|
38
40
|
|
|
39
41
|
private
|
|
40
42
|
|
|
43
|
+
def set_refresh_interval
|
|
44
|
+
@refresh_interval = params[:refresh].to_i if %w[30 60].include?(params[:refresh])
|
|
45
|
+
end
|
|
46
|
+
|
|
41
47
|
def turbo_stream_available?
|
|
42
48
|
Mime::Type.lookup_by_extension(:turbo_stream).present?
|
|
43
49
|
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
<style>
|
|
2
|
+
.trace-console { color-scheme: light dark; --background: #f8fafc; --surface: #fff; --text: #172033; --muted: #475569; --border: #cbd5e1; --link: #3730a3; --danger: #b91c1c; --success: #166534; margin: 0; background: var(--background); color: var(--text); font: 16px/1.5 system-ui, sans-serif; }
|
|
3
|
+
.trace-console * { box-sizing: border-box; }
|
|
4
|
+
.trace-console header { background: var(--surface); border-bottom: 1px solid var(--border); }
|
|
5
|
+
.trace-console nav, .trace-console main { max-width: 1200px; margin: auto; padding: 1rem; }
|
|
6
|
+
.trace-console nav, .trace-console .refresh-controls, .trace-console .tokens { display: flex; flex-wrap: wrap; gap: 1rem; align-items: center; }
|
|
7
|
+
.trace-console a { color: var(--link); text-underline-offset: .2em; }
|
|
8
|
+
.trace-console a:focus-visible, .trace-console summary:focus-visible { outline: 3px solid var(--link); outline-offset: 3px; }
|
|
9
|
+
.trace-console .muted, .trace-console dt { color: var(--muted); }
|
|
10
|
+
.trace-console .refresh-controls { justify-content: flex-end; margin-bottom: 1rem; }
|
|
11
|
+
.trace-console h1 { font-size: 1.5rem; }
|
|
12
|
+
.trace-console h2 { font-size: 1.2rem; }
|
|
13
|
+
.trace-console .panel { background: var(--surface); border: 1px solid var(--border); border-radius: .5rem; padding: 1rem; margin: 1rem 0; }
|
|
14
|
+
.trace-console .summary, .trace-console .metadata { display: grid; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); gap: 1rem; margin: 1rem 0; }
|
|
15
|
+
.trace-console .summary > div { padding: 1rem; background: var(--surface); border: 1px solid var(--border); border-radius: .5rem; }
|
|
16
|
+
.trace-console dt { font-size: .875rem; }
|
|
17
|
+
.trace-console dd { margin: 0; overflow-wrap: anywhere; }
|
|
18
|
+
.trace-console .summary dd { font-size: 1.4rem; font-weight: 600; }
|
|
19
|
+
.trace-console details { margin: .5rem 0; }
|
|
20
|
+
.trace-console summary { cursor: pointer; padding: .5rem 0; }
|
|
21
|
+
.trace-console .trace-summary { display: inline-flex; flex-wrap: wrap; gap: .5rem 1rem; align-items: baseline; }
|
|
22
|
+
.trace-console .badge { border: 1px solid currentColor; border-radius: .25rem; padding: .1rem .4rem; font-size: .8rem; }
|
|
23
|
+
.trace-console .error { color: var(--danger); }
|
|
24
|
+
.trace-console .success { color: var(--success); }
|
|
25
|
+
.trace-console .error-message { border-left: 4px solid var(--danger); padding: .5rem 1rem; overflow-wrap: anywhere; }
|
|
26
|
+
.trace-console pre { font: .8rem/1.5 ui-monospace, monospace; overflow: auto; padding: 1rem; background: var(--background); border: 1px solid var(--border); }
|
|
27
|
+
.trace-console .table-scroll { overflow-x: auto; }
|
|
28
|
+
.trace-console table { width: 100%; border-collapse: collapse; text-align: left; }
|
|
29
|
+
.trace-console th, .trace-console td { padding: .75rem; border-bottom: 1px solid var(--border); }
|
|
30
|
+
.trace-console .span-row { display: grid; grid-template-columns: minmax(5rem, 9rem) 1fr 5rem; gap: .5rem; align-items: center; margin: .5rem 0; font-size: .8rem; }
|
|
31
|
+
.trace-console .span-name { overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }
|
|
32
|
+
.trace-console .span-track { position: relative; height: 1.25rem; background: var(--background); border: 1px solid var(--border); overflow: hidden; border-radius: .2rem; }
|
|
33
|
+
.trace-console .span-bar { position: absolute; height: 100%; background: #6366f1; }
|
|
34
|
+
.trace-console .span-bar[data-type="tool"] { background: #b45309; }
|
|
35
|
+
.trace-console .span-bar[data-type="thinking"] { background: #9333ea; }
|
|
36
|
+
.trace-console .span-bar[data-type="response"] { background: #15803d; }
|
|
37
|
+
.trace-console .span-bar[data-status="ERROR"] { background: var(--danger); }
|
|
38
|
+
@media (prefers-color-scheme: dark) {
|
|
39
|
+
.trace-console { --background: #111827; --surface: #1f2937; --text: #f1f5f9; --muted: #cbd5e1; --border: #64748b; --link: #c7d2fe; --danger: #fca5a5; --success: #86efac; }
|
|
40
|
+
}
|
|
41
|
+
@media (max-width: 480px) {
|
|
42
|
+
.trace-console main { padding: .5rem; }
|
|
43
|
+
.trace-console .span-row { grid-template-columns: 5rem 1fr 4rem; }
|
|
44
|
+
}
|
|
45
|
+
</style>
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
<dl class="summary" aria-label="Last 24 hours">
|
|
2
|
+
<div><dt>Total Traces</dt><dd><%= number_with_delimiter(@metrics[:total_traces]) %></dd></div>
|
|
3
|
+
<div><dt>Total Tokens</dt><dd><%= number_to_human(@metrics[:total_tokens], precision: 2) %></dd></div>
|
|
4
|
+
<div><dt>Avg Duration</dt><dd><%= @metrics[:avg_duration_ms] >= 1000 ? "#{(@metrics[:avg_duration_ms] / 1000.0).round(2)}s" : "#{@metrics[:avg_duration_ms].round(0)}ms" %></dd></div>
|
|
5
|
+
<div><dt>Error Rate</dt><dd class="<%= 'error' if @metrics[:error_rate] > 5 %>"><%= @metrics[:error_rate] %>%</dd></div>
|
|
6
|
+
<div><dt>Active Agents</dt><dd><%= @metrics[:unique_agents] %></dd></div>
|
|
7
|
+
</dl>
|
|
8
|
+
<p class="muted">Last 24h</p>
|
|
@@ -1,64 +1,22 @@
|
|
|
1
|
-
<
|
|
2
|
-
|
|
3
|
-
<div
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
</div>
|
|
16
|
-
<div>
|
|
17
|
-
<span class="text-gray-500 dark:text-gray-400">Model</span>
|
|
18
|
-
<div class="text-gray-900 dark:text-white"><%= trace.model || "—" %></div>
|
|
19
|
-
</div>
|
|
20
|
-
</div>
|
|
21
|
-
|
|
22
|
-
<% if trace.error? && trace.error_message.present? %>
|
|
23
|
-
<div class="bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded p-3">
|
|
24
|
-
<div class="flex">
|
|
25
|
-
<svg class="h-5 w-5 text-red-400" viewBox="0 0 20 20" fill="currentColor">
|
|
26
|
-
<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" />
|
|
27
|
-
</svg>
|
|
28
|
-
<div class="ml-3">
|
|
29
|
-
<h3 class="text-sm font-medium text-red-800 dark:text-red-200">Error</h3>
|
|
30
|
-
<p class="mt-1 text-sm text-red-700 dark:text-red-300"><%= trace.error_message %></p>
|
|
31
|
-
</div>
|
|
32
|
-
</div>
|
|
33
|
-
</div>
|
|
1
|
+
<dl class="metadata">
|
|
2
|
+
<div><dt>Trace ID</dt><dd><%= trace.trace_id %></dd></div>
|
|
3
|
+
<div><dt>Service</dt><dd><%= trace.service_name || "Unknown" %></dd></div>
|
|
4
|
+
<div><dt>Provider</dt><dd><%= trace.provider || "Unknown" %></dd></div>
|
|
5
|
+
<div><dt>Model</dt><dd><%= trace.model || "Unknown" %></dd></div>
|
|
6
|
+
</dl>
|
|
7
|
+
<% if trace.error? && trace.error_message.present? %>
|
|
8
|
+
<p class="error-message error"><strong>Error:</strong> <%= trace.error_message %></p>
|
|
9
|
+
<% end %>
|
|
10
|
+
<div class="tokens">
|
|
11
|
+
<span>Input: <%= number_with_delimiter(trace.total_input_tokens || 0) %></span>
|
|
12
|
+
<span>Output: <%= number_with_delimiter(trace.total_output_tokens || 0) %></span>
|
|
13
|
+
<% if trace.total_thinking_tokens.to_i > 0 %>
|
|
14
|
+
<span>Thinking: <%= number_with_delimiter(trace.total_thinking_tokens) %></span>
|
|
34
15
|
<% end %>
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
<
|
|
38
|
-
<
|
|
39
|
-
<div class="w-3 h-3 rounded-full bg-blue-500"></div>
|
|
40
|
-
<span class="text-gray-500">Input:</span>
|
|
41
|
-
<span class="font-medium text-gray-900 dark:text-white"><%= number_with_delimiter(trace.total_input_tokens || 0) %></span>
|
|
42
|
-
</div>
|
|
43
|
-
<div class="flex items-center gap-2">
|
|
44
|
-
<div class="w-3 h-3 rounded-full bg-green-500"></div>
|
|
45
|
-
<span class="text-gray-500">Output:</span>
|
|
46
|
-
<span class="font-medium text-gray-900 dark:text-white"><%= number_with_delimiter(trace.total_output_tokens || 0) %></span>
|
|
47
|
-
</div>
|
|
48
|
-
<% if trace.total_thinking_tokens.to_i > 0 %>
|
|
49
|
-
<div class="flex items-center gap-2">
|
|
50
|
-
<div class="w-3 h-3 rounded-full bg-purple-500"></div>
|
|
51
|
-
<span class="text-gray-500">Thinking:</span>
|
|
52
|
-
<span class="font-medium text-purple-600 dark:text-purple-400"><%= number_with_delimiter(trace.total_thinking_tokens) %></span>
|
|
53
|
-
</div>
|
|
54
|
-
<% end %>
|
|
55
|
-
</div>
|
|
56
|
-
|
|
57
|
-
<!-- Spans Timeline -->
|
|
58
|
-
<% if trace.spans.present? %>
|
|
59
|
-
<div class="mt-4">
|
|
60
|
-
<h4 class="text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Span Timeline</h4>
|
|
61
|
-
<div class="relative bg-gray-100 dark:bg-gray-800 rounded-lg p-4">
|
|
16
|
+
</div>
|
|
17
|
+
<% if trace.spans.present? %>
|
|
18
|
+
<section aria-label="Span timeline">
|
|
19
|
+
<h2>Span Timeline</h2>
|
|
62
20
|
<% total_duration = (trace.total_duration_ms || 1).to_f %>
|
|
63
21
|
<% total_duration = 1.0 if total_duration.zero? %>
|
|
64
22
|
<%
|
|
@@ -77,41 +35,20 @@
|
|
|
77
35
|
start_pct = ((relative_ms || 0).to_f / total_duration * 100).clamp(0, 100)
|
|
78
36
|
width_pct = ((span["duration_ms"] || 0).to_f / total_duration * 100).clamp(0.5, [ 100 - start_pct, 0.5 ].max)
|
|
79
37
|
span_type = span["type"] || "unknown"
|
|
80
|
-
colors = {
|
|
81
|
-
"root" => "bg-gray-400",
|
|
82
|
-
"prompt" => "bg-blue-400",
|
|
83
|
-
"llm" => "bg-indigo-500",
|
|
84
|
-
"generate" => "bg-indigo-400",
|
|
85
|
-
"tool" => "bg-amber-500",
|
|
86
|
-
"thinking" => "bg-purple-500",
|
|
87
|
-
"response" => "bg-green-400"
|
|
88
|
-
}
|
|
89
|
-
bg_color = colors[span_type] || "bg-gray-400"
|
|
90
38
|
%>
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
</div>
|
|
100
|
-
</div>
|
|
101
|
-
<div class="w-16 text-xs text-gray-500 dark:text-gray-400 text-right">
|
|
102
|
-
<%= span["duration_ms"] ? "#{span["duration_ms"].round(1)}ms" : "—" %>
|
|
103
|
-
</div>
|
|
104
|
-
</div>
|
|
105
|
-
<% end %>
|
|
39
|
+
<div class="span-row">
|
|
40
|
+
<span class="span-name" title="<%= span["name"] %>"><%= span["name"]&.split(".")&.last || span_type %></span>
|
|
41
|
+
<div class="span-track">
|
|
42
|
+
<div class="span-bar" data-type="<%= span_type %>" data-status="<%= span["status"] %>"
|
|
43
|
+
style="left: <%= start_pct %>%; width: <%= width_pct %>%;"
|
|
44
|
+
title="<%= span["name"] %>: <%= span["duration_ms"]&.round(1) %>ms"></div>
|
|
45
|
+
</div>
|
|
46
|
+
<span><%= span["duration_ms"] ? "#{span["duration_ms"].round(1)}ms" : "Unknown" %></span>
|
|
106
47
|
</div>
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
<
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
</summary>
|
|
115
|
-
<pre class="mt-2 p-3 bg-gray-100 dark:bg-gray-800 rounded text-xs overflow-x-auto"><%= JSON.pretty_generate(trace.as_json(except: [:id, :created_at, :updated_at])) %></pre>
|
|
116
|
-
</details>
|
|
117
|
-
</div>
|
|
48
|
+
<% end %>
|
|
49
|
+
</section>
|
|
50
|
+
<% end %>
|
|
51
|
+
<details>
|
|
52
|
+
<summary>View raw trace data</summary>
|
|
53
|
+
<pre><%= JSON.pretty_generate(trace.as_json(except: [ :id, :created_at, :updated_at ])) %></pre>
|
|
54
|
+
</details>
|
|
@@ -1,135 +1,26 @@
|
|
|
1
|
-
<
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
<div class="text-sm font-medium text-gray-500 dark:text-gray-400">Avg Duration</div>
|
|
22
|
-
<div class="mt-1 text-2xl font-semibold text-gray-900 dark:text-white">
|
|
23
|
-
<%= @metrics[:avg_duration_ms] >= 1000 ? "#{(@metrics[:avg_duration_ms] / 1000.0).round(2)}s" : "#{@metrics[:avg_duration_ms].round(0)}ms" %>
|
|
24
|
-
</div>
|
|
25
|
-
<div class="text-xs text-gray-400">Last 24h</div>
|
|
26
|
-
</div>
|
|
27
|
-
|
|
28
|
-
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-4">
|
|
29
|
-
<div class="text-sm font-medium text-gray-500 dark:text-gray-400">Error Rate</div>
|
|
30
|
-
<div class="mt-1 text-2xl font-semibold <%= @metrics[:error_rate] > 5 ? 'text-red-600' : 'text-gray-900 dark:text-white' %>">
|
|
31
|
-
<%= @metrics[:error_rate] %>%
|
|
32
|
-
</div>
|
|
33
|
-
<div class="text-xs text-gray-400">Last 24h</div>
|
|
34
|
-
</div>
|
|
35
|
-
|
|
36
|
-
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-4">
|
|
37
|
-
<div class="text-sm font-medium text-gray-500 dark:text-gray-400">Active Agents</div>
|
|
38
|
-
<div class="mt-1 text-2xl font-semibold text-gray-900 dark:text-white">
|
|
39
|
-
<%= @metrics[:unique_agents] %>
|
|
40
|
-
</div>
|
|
41
|
-
<div class="text-xs text-gray-400">Last 24h</div>
|
|
42
|
-
</div>
|
|
43
|
-
</div>
|
|
44
|
-
|
|
45
|
-
<!-- Traces Table -->
|
|
46
|
-
<div class="bg-white dark:bg-gray-800 shadow rounded-lg overflow-hidden" data-controller="traces">
|
|
47
|
-
<div class="px-4 py-3 border-b border-gray-200 dark:border-gray-700 flex justify-between items-center">
|
|
48
|
-
<h2 class="text-lg font-medium text-gray-900 dark:text-white">Recent Traces</h2>
|
|
49
|
-
<div class="flex gap-2">
|
|
50
|
-
<%= link_to "All", traces_path, class: "px-3 py-1 text-sm rounded #{params[:status].blank? ? 'bg-indigo-100 text-indigo-700' : 'text-gray-600 hover:bg-gray-100'}" %>
|
|
51
|
-
<%= link_to "Errors", traces_path(status: "error"), class: "px-3 py-1 text-sm rounded #{params[:status] == 'error' ? 'bg-red-100 text-red-700' : 'text-gray-600 hover:bg-gray-100'}" %>
|
|
52
|
-
</div>
|
|
53
|
-
</div>
|
|
54
|
-
|
|
55
|
-
<% if @traces.any? %>
|
|
56
|
-
<table class="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
|
|
57
|
-
<thead class="bg-gray-50 dark:bg-gray-900">
|
|
58
|
-
<tr>
|
|
59
|
-
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Agent</th>
|
|
60
|
-
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Status</th>
|
|
61
|
-
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Duration</th>
|
|
62
|
-
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Tokens</th>
|
|
63
|
-
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Time</th>
|
|
64
|
-
</tr>
|
|
65
|
-
</thead>
|
|
66
|
-
<tbody class="bg-white dark:bg-gray-800 divide-y divide-gray-200 dark:divide-gray-700">
|
|
67
|
-
<% @traces.each do |trace| %>
|
|
68
|
-
<!-- Main Row -->
|
|
69
|
-
<tr class="hover:bg-gray-50 dark:hover:bg-gray-700 cursor-pointer"
|
|
70
|
-
data-trace-id="<%= trace.id %>"
|
|
71
|
-
data-action="click->traces#toggle">
|
|
72
|
-
<td class="px-4 py-3">
|
|
73
|
-
<div class="flex items-center">
|
|
74
|
-
<span class="text-sm font-medium text-gray-900 dark:text-white">
|
|
75
|
-
<%= trace.agent_class || "Unknown" %>
|
|
76
|
-
</span>
|
|
77
|
-
<% if trace.agent_action %>
|
|
78
|
-
<span class="ml-2 text-sm text-gray-500 dark:text-gray-400">
|
|
79
|
-
.<%= trace.agent_action %>
|
|
80
|
-
</span>
|
|
81
|
-
<% end %>
|
|
82
|
-
</div>
|
|
83
|
-
<div class="text-xs text-gray-400 font-mono"><%= trace.trace_id&.first(8) %></div>
|
|
84
|
-
</td>
|
|
85
|
-
<td class="px-4 py-3">
|
|
86
|
-
<% if trace.error? %>
|
|
87
|
-
<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-900 dark:text-red-200">
|
|
88
|
-
Error
|
|
89
|
-
</span>
|
|
90
|
-
<% else %>
|
|
91
|
-
<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-900 dark:text-green-200">
|
|
92
|
-
OK
|
|
93
|
-
</span>
|
|
94
|
-
<% end %>
|
|
95
|
-
</td>
|
|
96
|
-
<td class="px-4 py-3 text-sm text-gray-900 dark:text-white">
|
|
97
|
-
<%= trace.formatted_duration %>
|
|
98
|
-
</td>
|
|
99
|
-
<td class="px-4 py-3">
|
|
100
|
-
<div class="flex items-center gap-2 text-sm">
|
|
101
|
-
<span class="text-gray-900 dark:text-white"><%= trace.formatted_tokens %></span>
|
|
102
|
-
<% if trace.total_thinking_tokens.to_i > 0 %>
|
|
103
|
-
<span class="text-xs text-purple-600 dark:text-purple-400" title="Thinking tokens">
|
|
104
|
-
+<%= number_to_human(trace.total_thinking_tokens, precision: 1) %> thinking
|
|
105
|
-
</span>
|
|
106
|
-
<% end %>
|
|
107
|
-
</div>
|
|
108
|
-
</td>
|
|
109
|
-
<td class="px-4 py-3 text-sm text-gray-500 dark:text-gray-400">
|
|
110
|
-
<%= time_ago_in_words(trace.timestamp || trace.created_at) %> ago
|
|
111
|
-
</td>
|
|
112
|
-
</tr>
|
|
113
|
-
|
|
114
|
-
<!-- Expanded Detail Row -->
|
|
115
|
-
<tr id="detail-<%= trace.id %>" class="hidden bg-gray-50 dark:bg-gray-900">
|
|
116
|
-
<td colspan="5" class="px-4 py-4">
|
|
117
|
-
<%= render partial: "trace_detail", locals: { trace: trace } %>
|
|
118
|
-
</td>
|
|
119
|
-
</tr>
|
|
120
|
-
<% end %>
|
|
121
|
-
</tbody>
|
|
122
|
-
</table>
|
|
123
|
-
<% else %>
|
|
124
|
-
<div class="px-4 py-12 text-center">
|
|
125
|
-
<svg class="mx-auto h-12 w-12 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
126
|
-
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 17v-2m3 2v-4m3 4v-6m2 10H7a2 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" />
|
|
127
|
-
</svg>
|
|
128
|
-
<h3 class="mt-2 text-sm font-medium text-gray-900 dark:text-white">No traces yet</h3>
|
|
129
|
-
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
|
|
130
|
-
Traces will appear here when your agents generate responses.
|
|
131
|
-
</p>
|
|
132
|
-
</div>
|
|
1
|
+
<h1>Traces</h1>
|
|
2
|
+
<%= render "summary" %>
|
|
3
|
+
<section class="panel" aria-label="Recent traces">
|
|
4
|
+
<h2>Recent Traces</h2>
|
|
5
|
+
<p><%= link_to "All", traces_path %> · <%= link_to "Errors", traces_path(status: "error") %></p>
|
|
6
|
+
<% if @traces.any? %>
|
|
7
|
+
<% @traces.each do |trace| %>
|
|
8
|
+
<details class="panel">
|
|
9
|
+
<summary>
|
|
10
|
+
<span class="trace-summary">
|
|
11
|
+
<strong><%= trace.display_name %></strong>
|
|
12
|
+
<span class="badge <%= trace.error? ? 'error' : 'success' %>"><%= trace.error? ? "Error" : "OK" %></span>
|
|
13
|
+
<span><%= trace.formatted_duration %></span>
|
|
14
|
+
<span><%= trace.formatted_tokens %> tokens</span>
|
|
15
|
+
<span class="muted"><%= time_ago_in_words(trace.timestamp || trace.created_at) %> ago</span>
|
|
16
|
+
</span>
|
|
17
|
+
</summary>
|
|
18
|
+
<%= link_to "Open trace", trace_path(trace) %>
|
|
19
|
+
<%= render "trace_detail", trace: trace %>
|
|
20
|
+
</details>
|
|
133
21
|
<% end %>
|
|
134
|
-
|
|
135
|
-
</
|
|
22
|
+
<% else %>
|
|
23
|
+
<h2>No traces yet</h2>
|
|
24
|
+
<p>Traces will appear here when your agents generate responses.</p>
|
|
25
|
+
<% end %>
|
|
26
|
+
</section>
|
|
@@ -1,145 +1,27 @@
|
|
|
1
|
-
<
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
<
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
</svg>
|
|
12
|
-
</div>
|
|
13
|
-
<div class="ml-4">
|
|
14
|
-
<p class="text-sm font-medium text-gray-500 dark:text-gray-400">Total Traces</p>
|
|
15
|
-
<p class="text-2xl font-semibold text-gray-900 dark:text-white">
|
|
16
|
-
<%= number_with_delimiter(@metrics[:total_traces]) %>
|
|
17
|
-
</p>
|
|
18
|
-
</div>
|
|
19
|
-
</div>
|
|
20
|
-
</div>
|
|
21
|
-
|
|
22
|
-
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
|
|
23
|
-
<div class="flex items-center">
|
|
24
|
-
<div class="p-3 rounded-full bg-blue-100 dark:bg-blue-900">
|
|
25
|
-
<svg class="h-6 w-6 text-blue-600 dark:text-blue-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
26
|
-
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 7h.01M7 3h5c.512 0 1.024.195 1.414.586l7 7a2 2 0 010 2.828l-7 7a2 2 0 01-2.828 0l-7-7A1.994 1.994 0 013 12V7a4 4 0 014-4z" />
|
|
27
|
-
</svg>
|
|
28
|
-
</div>
|
|
29
|
-
<div class="ml-4">
|
|
30
|
-
<p class="text-sm font-medium text-gray-500 dark:text-gray-400">Total Tokens</p>
|
|
31
|
-
<p class="text-2xl font-semibold text-gray-900 dark:text-white">
|
|
32
|
-
<%= number_to_human(@metrics[:total_tokens], precision: 2) %>
|
|
33
|
-
</p>
|
|
34
|
-
</div>
|
|
35
|
-
</div>
|
|
36
|
-
</div>
|
|
37
|
-
|
|
38
|
-
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
|
|
39
|
-
<div class="flex items-center">
|
|
40
|
-
<div class="p-3 rounded-full bg-green-100 dark:bg-green-900">
|
|
41
|
-
<svg class="h-6 w-6 text-green-600 dark:text-green-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
42
|
-
<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" />
|
|
43
|
-
</svg>
|
|
44
|
-
</div>
|
|
45
|
-
<div class="ml-4">
|
|
46
|
-
<p class="text-sm font-medium text-gray-500 dark:text-gray-400">Avg Duration</p>
|
|
47
|
-
<p class="text-2xl font-semibold text-gray-900 dark:text-white">
|
|
48
|
-
<%= @metrics[:avg_duration_ms] >= 1000 ? "#{(@metrics[:avg_duration_ms] / 1000.0).round(2)}s" : "#{@metrics[:avg_duration_ms].round(0)}ms" %>
|
|
49
|
-
</p>
|
|
50
|
-
</div>
|
|
51
|
-
</div>
|
|
52
|
-
</div>
|
|
53
|
-
|
|
54
|
-
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
|
|
55
|
-
<div class="flex items-center">
|
|
56
|
-
<div class="p-3 rounded-full <%= @metrics[:error_rate] > 5 ? 'bg-red-100 dark:bg-red-900' : 'bg-gray-100 dark:bg-gray-700' %>">
|
|
57
|
-
<svg class="h-6 w-6 <%= @metrics[:error_rate] > 5 ? 'text-red-600 dark:text-red-400' : 'text-gray-600 dark:text-gray-400' %>" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
58
|
-
<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" />
|
|
59
|
-
</svg>
|
|
60
|
-
</div>
|
|
61
|
-
<div class="ml-4">
|
|
62
|
-
<p class="text-sm font-medium text-gray-500 dark:text-gray-400">Error Rate</p>
|
|
63
|
-
<p class="text-2xl font-semibold <%= @metrics[:error_rate] > 5 ? 'text-red-600' : 'text-gray-900 dark:text-white' %>">
|
|
64
|
-
<%= @metrics[:error_rate] %>%
|
|
65
|
-
</p>
|
|
66
|
-
</div>
|
|
67
|
-
</div>
|
|
68
|
-
</div>
|
|
69
|
-
|
|
70
|
-
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
|
|
71
|
-
<div class="flex items-center">
|
|
72
|
-
<div class="p-3 rounded-full bg-purple-100 dark:bg-purple-900">
|
|
73
|
-
<svg class="h-6 w-6 text-purple-600 dark:text-purple-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
74
|
-
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z" />
|
|
75
|
-
</svg>
|
|
76
|
-
</div>
|
|
77
|
-
<div class="ml-4">
|
|
78
|
-
<p class="text-sm font-medium text-gray-500 dark:text-gray-400">Active Agents</p>
|
|
79
|
-
<p class="text-2xl font-semibold text-gray-900 dark:text-white">
|
|
80
|
-
<%= @metrics[:unique_agents] %>
|
|
81
|
-
</p>
|
|
82
|
-
</div>
|
|
83
|
-
</div>
|
|
84
|
-
</div>
|
|
85
|
-
</div>
|
|
86
|
-
|
|
87
|
-
<!-- Agent Statistics -->
|
|
88
|
-
<div class="bg-white dark:bg-gray-800 shadow rounded-lg overflow-hidden mb-8">
|
|
89
|
-
<div class="px-4 py-4 border-b border-gray-200 dark:border-gray-700">
|
|
90
|
-
<h2 class="text-lg font-medium text-gray-900 dark:text-white">Agent Statistics</h2>
|
|
91
|
-
<p class="text-sm text-gray-500 dark:text-gray-400">Performance breakdown by agent class (last 24h)</p>
|
|
92
|
-
</div>
|
|
93
|
-
|
|
94
|
-
<% if @agent_stats.any? %>
|
|
95
|
-
<table class="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
|
|
96
|
-
<thead class="bg-gray-50 dark:bg-gray-900">
|
|
97
|
-
<tr>
|
|
98
|
-
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Agent</th>
|
|
99
|
-
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Traces</th>
|
|
100
|
-
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Tokens</th>
|
|
101
|
-
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Avg Duration</th>
|
|
102
|
-
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Errors</th>
|
|
103
|
-
</tr>
|
|
104
|
-
</thead>
|
|
105
|
-
<tbody class="bg-white dark:bg-gray-800 divide-y divide-gray-200 dark:divide-gray-700">
|
|
1
|
+
<h1>Metrics Overview</h1>
|
|
2
|
+
<%= render "summary" %>
|
|
3
|
+
<section class="panel">
|
|
4
|
+
<h2>Agent Statistics</h2>
|
|
5
|
+
<p class="muted">Performance breakdown by agent class (last 24h)</p>
|
|
6
|
+
<% if @agent_stats.any? %>
|
|
7
|
+
<div class="table-scroll">
|
|
8
|
+
<table>
|
|
9
|
+
<thead><tr><th scope="col">Agent</th><th scope="col">Traces</th><th scope="col">Tokens</th><th scope="col">Avg Duration</th><th scope="col">Errors</th></tr></thead>
|
|
10
|
+
<tbody>
|
|
106
11
|
<% @agent_stats.each do |stat| %>
|
|
107
|
-
<tr
|
|
108
|
-
<
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
<td class="
|
|
114
|
-
<%= number_with_delimiter(stat.trace_count) %>
|
|
115
|
-
</td>
|
|
116
|
-
<td class="px-4 py-3 text-sm text-gray-900 dark:text-white">
|
|
117
|
-
<%# stat is a grouped row: read the SQL alias, not the model
|
|
118
|
-
method (which sums per-trace token columns absent here) %>
|
|
119
|
-
<%= number_to_human(stat[:total_tokens] || 0, precision: 1) %>
|
|
120
|
-
</td>
|
|
121
|
-
<td class="px-4 py-3 text-sm text-gray-900 dark:text-white">
|
|
122
|
-
<% avg = stat.avg_duration.to_f %>
|
|
123
|
-
<%= avg >= 1000 ? "#{(avg / 1000.0).round(2)}s" : "#{avg.round(0)}ms" %>
|
|
124
|
-
</td>
|
|
125
|
-
<td class="px-4 py-3">
|
|
126
|
-
<% error_count = stat.error_count.to_i %>
|
|
127
|
-
<% if error_count > 0 %>
|
|
128
|
-
<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-900 dark:text-red-200">
|
|
129
|
-
<%= error_count %>
|
|
130
|
-
</span>
|
|
131
|
-
<% else %>
|
|
132
|
-
<span class="text-sm text-gray-400">0</span>
|
|
133
|
-
<% end %>
|
|
134
|
-
</td>
|
|
12
|
+
<tr>
|
|
13
|
+
<th scope="row"><%= stat.agent_class || "Unknown" %></th>
|
|
14
|
+
<td><%= number_with_delimiter(stat.trace_count) %></td>
|
|
15
|
+
<td><%= number_to_human(stat[:total_tokens] || 0, precision: 1) %></td>
|
|
16
|
+
<% avg = stat.avg_duration.to_f %>
|
|
17
|
+
<td><%= avg >= 1000 ? "#{(avg / 1000.0).round(2)}s" : "#{avg.round(0)}ms" %></td>
|
|
18
|
+
<td class="<%= 'error' if stat.error_count.to_i > 0 %>"><%= stat.error_count.to_i %></td>
|
|
135
19
|
</tr>
|
|
136
20
|
<% end %>
|
|
137
21
|
</tbody>
|
|
138
22
|
</table>
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
</div>
|
|
145
|
-
</div>
|
|
23
|
+
</div>
|
|
24
|
+
<% else %>
|
|
25
|
+
<p>No agent data available yet.</p>
|
|
26
|
+
<% end %>
|
|
27
|
+
</section>
|
|
@@ -1,36 +1,7 @@
|
|
|
1
|
-
<
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
<
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
<div>
|
|
9
|
-
<h1 class="text-xl font-semibold text-gray-900 dark:text-white">
|
|
10
|
-
<%= @trace.display_name %>
|
|
11
|
-
</h1>
|
|
12
|
-
<p class="text-sm text-gray-500 dark:text-gray-400 font-mono">
|
|
13
|
-
<%= @trace.trace_id %>
|
|
14
|
-
</p>
|
|
15
|
-
</div>
|
|
16
|
-
<div class="flex items-center gap-4">
|
|
17
|
-
<% if @trace.error? %>
|
|
18
|
-
<span class="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200">
|
|
19
|
-
Error
|
|
20
|
-
</span>
|
|
21
|
-
<% else %>
|
|
22
|
-
<span class="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200">
|
|
23
|
-
Success
|
|
24
|
-
</span>
|
|
25
|
-
<% end %>
|
|
26
|
-
<span class="text-sm text-gray-500">
|
|
27
|
-
<%= @trace.timestamp&.strftime("%Y-%m-%d %H:%M:%S") %>
|
|
28
|
-
</span>
|
|
29
|
-
</div>
|
|
30
|
-
</div>
|
|
31
|
-
</div>
|
|
32
|
-
|
|
33
|
-
<div class="p-4">
|
|
34
|
-
<%= render partial: "trace_detail", locals: { trace: @trace } %>
|
|
35
|
-
</div>
|
|
36
|
-
</div>
|
|
1
|
+
<p><%= link_to "Back to traces", traces_path %></p>
|
|
2
|
+
<article class="panel">
|
|
3
|
+
<h1><%= @trace.display_name %></h1>
|
|
4
|
+
<p class="badge <%= @trace.error? ? 'error' : 'success' %>"><%= @trace.error? ? "Error" : "Success" %></p>
|
|
5
|
+
<p><time><%= @trace.timestamp&.strftime("%Y-%m-%d %H:%M:%S") %></time></p>
|
|
6
|
+
<%= render "trace_detail", trace: @trace %>
|
|
7
|
+
</article>
|