activeagent 1.1.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +61 -0
  3. data/README.md +24 -14
  4. data/lib/active_agent/telemetry/configuration.rb +13 -9
  5. data/lib/active_agent/telemetry/instrumentation.rb +4 -0
  6. data/lib/active_agent/telemetry/tool_origin.rb +90 -0
  7. data/lib/active_agent/telemetry.rb +1 -0
  8. data/lib/active_agent/version.rb +1 -1
  9. data/lib/active_agent.rb +0 -5
  10. metadata +22 -32
  11. data/lib/active_agent/dashboard/app/controllers/active_agent/dashboard/api/traces_controller.rb +0 -138
  12. data/lib/active_agent/dashboard/app/controllers/active_agent/dashboard/application_controller.rb +0 -64
  13. data/lib/active_agent/dashboard/app/controllers/active_agent/dashboard/dashboard_controller.rb +0 -129
  14. data/lib/active_agent/dashboard/app/controllers/active_agent/dashboard/traces_controller.rb +0 -123
  15. data/lib/active_agent/dashboard/app/jobs/active_agent/dashboard/agent_execution_job.rb +0 -56
  16. data/lib/active_agent/dashboard/app/jobs/active_agent/dashboard/application_job.rb +0 -14
  17. data/lib/active_agent/dashboard/app/jobs/active_agent/dashboard/sandbox_cleanup_job.rb +0 -49
  18. data/lib/active_agent/dashboard/app/jobs/active_agent/dashboard/sandbox_provision_job.rb +0 -65
  19. data/lib/active_agent/dashboard/app/jobs/active_agent/process_telemetry_traces_job.rb +0 -86
  20. data/lib/active_agent/dashboard/app/models/active_agent/dashboard/agent.rb +0 -256
  21. data/lib/active_agent/dashboard/app/models/active_agent/dashboard/agent_run.rb +0 -113
  22. data/lib/active_agent/dashboard/app/models/active_agent/dashboard/agent_template.rb +0 -208
  23. data/lib/active_agent/dashboard/app/models/active_agent/dashboard/agent_version.rb +0 -60
  24. data/lib/active_agent/dashboard/app/models/active_agent/dashboard/application_record.rb +0 -46
  25. data/lib/active_agent/dashboard/app/models/active_agent/dashboard/recording_action.rb +0 -125
  26. data/lib/active_agent/dashboard/app/models/active_agent/dashboard/recording_snapshot.rb +0 -83
  27. data/lib/active_agent/dashboard/app/models/active_agent/dashboard/sandbox_run.rb +0 -52
  28. data/lib/active_agent/dashboard/app/models/active_agent/dashboard/sandbox_session.rb +0 -169
  29. data/lib/active_agent/dashboard/app/models/active_agent/dashboard/session_recording.rb +0 -193
  30. data/lib/active_agent/dashboard/app/models/active_agent/telemetry_trace.rb +0 -214
  31. data/lib/active_agent/dashboard/app/views/active_agent/dashboard/traces/_trace_detail.html.erb +0 -117
  32. data/lib/active_agent/dashboard/app/views/active_agent/dashboard/traces/index.html.erb +0 -135
  33. data/lib/active_agent/dashboard/app/views/active_agent/dashboard/traces/metrics.html.erb +0 -145
  34. data/lib/active_agent/dashboard/app/views/active_agent/dashboard/traces/show.html.erb +0 -36
  35. data/lib/active_agent/dashboard/app/views/layouts/active_agent/dashboard/application.html.erb +0 -94
  36. data/lib/active_agent/dashboard/config/routes.rb +0 -19
  37. data/lib/active_agent/dashboard/engine.rb +0 -43
  38. data/lib/active_agent/dashboard.rb +0 -161
  39. data/lib/generators/active_agent/dashboard/install_generator.rb +0 -92
  40. data/lib/generators/active_agent/dashboard/templates/active_agent_dashboard.rb.erb +0 -67
  41. data/lib/generators/active_agent/dashboard/templates/create_active_agent_telemetry_traces.rb.erb +0 -46
@@ -1,214 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module ActiveAgent
4
- # Stores telemetry traces from ActiveAgent clients.
5
- #
6
- # Each trace represents a complete generation lifecycle, including prompt
7
- # preparation, LLM calls, tool invocations, and error handling.
8
- #
9
- # This model supports two modes:
10
- # - Local mode: No account association (single-tenant, self-hosted)
11
- # - Multi-tenant mode: With account association (for activeagents.ai platform)
12
- #
13
- # @example Creating a trace from ingested data (local mode)
14
- # ActiveAgent::TelemetryTrace.create_from_payload(trace_payload, sdk_info)
15
- #
16
- # @example Creating a trace with account (multi-tenant mode)
17
- # ActiveAgent::TelemetryTrace.create_from_payload(trace_payload, sdk_info, account: account)
18
- #
19
- class TelemetryTrace < ::ActiveRecord::Base
20
- self.table_name = "active_agent_telemetry_traces"
21
-
22
- # Optional account association for multi-tenant mode
23
- # The host app can add: belongs_to :account if needed
24
- if ActiveAgent::Dashboard.multi_tenant?
25
- belongs_to :account, class_name: ActiveAgent::Dashboard.account_class
26
- end
27
-
28
- # Status values for traces
29
- STATUS_OK = "OK"
30
- STATUS_ERROR = "ERROR"
31
- STATUS_UNSET = "UNSET"
32
-
33
- validates :trace_id, presence: true
34
-
35
- # Scopes
36
- scope :recent, -> { order(timestamp: :desc) }
37
- scope :with_errors, -> { where(status: STATUS_ERROR) }
38
- scope :for_service, ->(name) { where(service_name: name) }
39
- scope :for_environment, ->(env) { where(environment: env) }
40
- scope :for_agent, ->(agent_class) { where(agent_class: agent_class) }
41
- scope :for_date_range, ->(start_date, end_date) { where(timestamp: start_date..end_date) }
42
- scope :for_account, ->(account) { where(account: account) if ActiveAgent::Dashboard.multi_tenant? }
43
-
44
- # Creates a TelemetryTrace from an ingested trace payload.
45
- #
46
- # Extracts relevant data from the trace payload and stores it in a
47
- # normalized format for querying and analysis.
48
- #
49
- # @param trace [Hash] The trace payload from ActiveAgent::Telemetry
50
- # @param sdk_info [Hash] SDK metadata
51
- # @param account [Object, nil] Optional account for multi-tenant mode
52
- # @return [TelemetryTrace] The created trace
53
- def self.create_from_payload(trace, sdk_info = {}, account: nil)
54
- spans = trace["spans"] || []
55
- root_span = spans.find { |s| s["parent_span_id"].nil? } || spans.first || {}
56
-
57
- total_duration = root_span["duration_ms"]
58
-
59
- # Instrumentation mirrors LLM token usage onto the root span for
60
- # display, so summing every span double-counts. When child spans
61
- # carry token data, they are the source of truth; the root span only
62
- # counts for single-span traces.
63
- counted_spans = spans.reject { |s| s["parent_span_id"].nil? }
64
- counted_spans = spans if counted_spans.none? { |s| span_token_sum(s).positive? }
65
-
66
- total_input = 0
67
- total_output = 0
68
- total_thinking = 0
69
-
70
- counted_spans.each do |span|
71
- tokens = span["tokens"] || {}
72
- total_input += (tokens["input"] || 0)
73
- total_output += (tokens["output"] || 0)
74
- total_thinking += (tokens["thinking"] || 0)
75
- end
76
-
77
- # Extract agent info from root span attributes
78
- attributes = root_span["attributes"] || {}
79
- agent_class = attributes["agent.class"]
80
- agent_action = attributes["agent.action"]
81
-
82
- # Find any error message
83
- error_span = spans.find { |s| s["status"] == STATUS_ERROR }
84
- error_message = error_span&.dig("attributes", "error.message")
85
-
86
- attrs = {
87
- trace_id: trace["trace_id"],
88
- service_name: trace["service_name"],
89
- environment: trace["environment"],
90
- timestamp: Time.parse(trace["timestamp"]),
91
- spans: spans,
92
- resource_attributes: trace["resource_attributes"],
93
- sdk_info: sdk_info,
94
- total_duration_ms: total_duration,
95
- total_input_tokens: total_input,
96
- total_output_tokens: total_output,
97
- total_thinking_tokens: total_thinking,
98
- status: root_span["status"] || STATUS_UNSET,
99
- agent_class: agent_class,
100
- agent_action: agent_action,
101
- error_message: error_message
102
- }
103
-
104
- # Add account if in multi-tenant mode
105
- attrs[:account] = account if ActiveAgent::Dashboard.multi_tenant? && account
106
-
107
- create!(attrs)
108
- end
109
-
110
- # Sums a span's token counts (used to decide which spans carry the
111
- # authoritative token data during ingestion).
112
- #
113
- # @api private
114
- def self.span_token_sum(span)
115
- tokens = span["tokens"] || {}
116
- tokens.fetch("input", 0).to_i + tokens.fetch("output", 0).to_i + tokens.fetch("thinking", 0).to_i
117
- end
118
-
119
- # Returns the root span of this trace.
120
- #
121
- # @return [Hash, nil] The root span or nil
122
- def root_span
123
- spans&.find { |s| s["parent_span_id"].nil? }
124
- end
125
-
126
- # Returns all LLM spans in this trace.
127
- #
128
- # @return [Array<Hash>] LLM spans
129
- def llm_spans
130
- spans&.select { |s| s["type"] == "llm" } || []
131
- end
132
-
133
- # Returns all tool call spans in this trace.
134
- #
135
- # @return [Array<Hash>] Tool spans
136
- def tool_spans
137
- spans&.select { |s| s["type"] == "tool" } || []
138
- end
139
-
140
- # Returns total token count.
141
- #
142
- # @return [Integer] Total tokens used
143
- def total_tokens
144
- (total_input_tokens || 0) + (total_output_tokens || 0) + (total_thinking_tokens || 0)
145
- end
146
-
147
- # Returns whether this trace had an error.
148
- #
149
- # @return [Boolean]
150
- def error?
151
- status == STATUS_ERROR
152
- end
153
-
154
- # Returns display name for the trace.
155
- #
156
- # @return [String] Display name (e.g., "WeatherAgent.forecast")
157
- def display_name
158
- if agent_class && agent_action
159
- "#{agent_class}.#{agent_action}"
160
- elsif agent_class
161
- agent_class
162
- else
163
- trace_id&.first(8)
164
- end
165
- end
166
-
167
- # Returns formatted duration.
168
- #
169
- # @return [String] Duration in ms or s
170
- def formatted_duration
171
- return "—" unless total_duration_ms
172
-
173
- if total_duration_ms >= 1000
174
- "#{(total_duration_ms / 1000.0).round(2)}s"
175
- else
176
- "#{total_duration_ms.round(0)}ms"
177
- end
178
- end
179
-
180
- # Returns formatted token count.
181
- #
182
- # @return [String] Token count with K suffix for large numbers
183
- def formatted_tokens
184
- count = total_tokens
185
- return "0" if count.zero?
186
-
187
- if count >= 1000
188
- "#{(count / 1000.0).round(1)}K"
189
- else
190
- count.to_s
191
- end
192
- end
193
-
194
- # Returns the provider used (from LLM spans).
195
- #
196
- # @return [String, nil] Provider name
197
- def provider
198
- llm_span = llm_spans.first
199
- return nil unless llm_span
200
-
201
- llm_span.dig("attributes", "llm.provider")
202
- end
203
-
204
- # Returns the model used (from LLM spans).
205
- #
206
- # @return [String, nil] Model name
207
- def model
208
- llm_span = llm_spans.first
209
- return nil unless llm_span
210
-
211
- llm_span.dig("attributes", "llm.model")
212
- end
213
- end
214
- end
@@ -1,117 +0,0 @@
1
- <div class="space-y-4">
2
- <!-- Trace Info -->
3
- <div class="grid grid-cols-2 md:grid-cols-4 gap-4 text-sm">
4
- <div>
5
- <span class="text-gray-500 dark:text-gray-400">Trace ID</span>
6
- <div class="font-mono text-gray-900 dark:text-white"><%= trace.trace_id %></div>
7
- </div>
8
- <div>
9
- <span class="text-gray-500 dark:text-gray-400">Service</span>
10
- <div class="text-gray-900 dark:text-white"><%= trace.service_name || "—" %></div>
11
- </div>
12
- <div>
13
- <span class="text-gray-500 dark:text-gray-400">Provider</span>
14
- <div class="text-gray-900 dark:text-white"><%= trace.provider || "—" %></div>
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>
34
- <% end %>
35
-
36
- <!-- Token Breakdown -->
37
- <div class="flex gap-6 text-sm">
38
- <div class="flex items-center gap-2">
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">
62
- <% total_duration = (trace.total_duration_ms || 1).to_f %>
63
- <% total_duration = 1.0 if total_duration.zero? %>
64
- <%
65
- # Offsets relative to the root span's start. Span payloads carry
66
- # absolute ISO8601 start_time values, not relative offsets.
67
- root = trace.root_span || trace.spans.first || {}
68
- root_start = Time.parse(root["start_time"]) rescue nil
69
- %>
70
- <% trace.spans.each_with_index do |span, index| %>
71
- <%
72
- relative_ms = span["start_time_relative_ms"]
73
- if relative_ms.nil? && root_start && span["start_time"].present?
74
- span_start = Time.parse(span["start_time"]) rescue nil
75
- relative_ms = ((span_start - root_start) * 1000).round(2) if span_start
76
- end
77
- start_pct = ((relative_ms || 0).to_f / total_duration * 100).clamp(0, 100)
78
- width_pct = ((span["duration_ms"] || 0).to_f / total_duration * 100).clamp(0.5, [ 100 - start_pct, 0.5 ].max)
79
- 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
- %>
91
- <div class="flex items-center gap-2 mb-2">
92
- <div class="w-24 text-xs text-gray-500 dark:text-gray-400 truncate" title="<%= span["name"] %>">
93
- <%= span["name"]&.split(".")&.last || span_type %>
94
- </div>
95
- <div class="flex-1 h-6 bg-gray-200 dark:bg-gray-700 rounded relative overflow-hidden">
96
- <div class="span-bar absolute h-full <%= bg_color %> rounded <%= span["status"] == "ERROR" ? 'opacity-50 bg-red-500' : '' %>"
97
- style="left: <%= start_pct %>%; width: <%= width_pct %>%;"
98
- title="<%= span["name"] %>: <%= span["duration_ms"]&.round(1) %>ms">
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 %>
106
- </div>
107
- </div>
108
- <% end %>
109
-
110
- <!-- Raw Data (collapsed) -->
111
- <details class="mt-4">
112
- <summary class="text-sm text-gray-500 dark:text-gray-400 cursor-pointer hover:text-gray-700">
113
- View raw trace data
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>
@@ -1,135 +0,0 @@
1
- <div data-controller="refresh" data-refresh-interval-value="30000">
2
- <!-- Metrics Summary -->
3
- <div class="grid grid-cols-1 md:grid-cols-5 gap-4 mb-6">
4
- <div class="bg-white dark:bg-gray-800 rounded-lg shadow p-4">
5
- <div class="text-sm font-medium text-gray-500 dark:text-gray-400">Total Traces</div>
6
- <div class="mt-1 text-2xl font-semibold text-gray-900 dark:text-white">
7
- <%= number_with_delimiter(@metrics[:total_traces]) %>
8
- </div>
9
- <div class="text-xs text-gray-400">Last 24h</div>
10
- </div>
11
-
12
- <div class="bg-white dark:bg-gray-800 rounded-lg shadow p-4">
13
- <div class="text-sm font-medium text-gray-500 dark:text-gray-400">Total Tokens</div>
14
- <div class="mt-1 text-2xl font-semibold text-gray-900 dark:text-white">
15
- <%= number_to_human(@metrics[:total_tokens], precision: 2) %>
16
- </div>
17
- <div class="text-xs text-gray-400">Last 24h</div>
18
- </div>
19
-
20
- <div class="bg-white dark:bg-gray-800 rounded-lg shadow p-4">
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>
133
- <% end %>
134
- </div>
135
- </div>
@@ -1,145 +0,0 @@
1
- <div data-controller="refresh" data-refresh-interval-value="60000">
2
- <h1 class="text-2xl font-bold text-gray-900 dark:text-white mb-6">Metrics Overview</h1>
3
-
4
- <!-- Summary Cards -->
5
- <div class="grid grid-cols-1 md:grid-cols-5 gap-4 mb-8">
6
- <div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
7
- <div class="flex items-center">
8
- <div class="p-3 rounded-full bg-indigo-100 dark:bg-indigo-900">
9
- <svg class="h-6 w-6 text-indigo-600 dark:text-indigo-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
10
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" />
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">
106
- <% @agent_stats.each do |stat| %>
107
- <tr class="hover:bg-gray-50 dark:hover:bg-gray-700">
108
- <td class="px-4 py-3">
109
- <span class="text-sm font-medium text-gray-900 dark:text-white">
110
- <%= stat.agent_class || "Unknown" %>
111
- </span>
112
- </td>
113
- <td class="px-4 py-3 text-sm text-gray-900 dark:text-white">
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>
135
- </tr>
136
- <% end %>
137
- </tbody>
138
- </table>
139
- <% else %>
140
- <div class="px-4 py-8 text-center text-gray-500 dark:text-gray-400">
141
- No agent data available yet.
142
- </div>
143
- <% end %>
144
- </div>
145
- </div>
@@ -1,36 +0,0 @@
1
- <div class="mb-4">
2
- <%= link_to "← Back to traces", traces_path, class: "text-indigo-600 hover:text-indigo-800 text-sm" %>
3
- </div>
4
-
5
- <div class="bg-white dark:bg-gray-800 shadow rounded-lg overflow-hidden">
6
- <div class="px-4 py-4 border-b border-gray-200 dark:border-gray-700">
7
- <div class="flex items-center justify-between">
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>