ai-agent-inspector 1.0.0__py3-none-any.whl

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.
@@ -0,0 +1,441 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Agent Inspector</title>
7
+ <link rel="stylesheet" href="/ui/static/app.css">
8
+ </head>
9
+ <body>
10
+ <button class="theme-toggle" id="themeToggle" type="button">Theme: Auto</button>
11
+ <div class="container">
12
+ <!-- Left Panel: Run List -->
13
+ <div class="panel">
14
+ <div class="panel-header">Runs</div>
15
+ <div class="filters">
16
+ <input type="text" class="search-input" id="searchInput" placeholder="Search runs...">
17
+ <select class="filter-select" id="statusFilter">
18
+ <option value="">All Status</option>
19
+ <option value="completed">Completed</option>
20
+ <option value="running">Running</option>
21
+ <option value="failed">Failed</option>
22
+ </select>
23
+ <select class="filter-select" id="eventTypeFilter">
24
+ <option value="">All Events</option>
25
+ <option value="llm_call">LLM Calls</option>
26
+ <option value="tool_call">Tool Calls</option>
27
+ <option value="error">Errors</option>
28
+ </select>
29
+ </div>
30
+ <div class="panel-content">
31
+ <ul class="run-list" id="runList">
32
+ <li class="loading">Loading runs...</li>
33
+ </ul>
34
+ </div>
35
+ </div>
36
+
37
+ <!-- Center Panel: Timeline -->
38
+ <div class="panel">
39
+ <div class="panel-header">Timeline</div>
40
+ <div class="panel-content">
41
+ <div class="timeline" id="timeline">
42
+ <div class="loading">Select a run to view timeline</div>
43
+ </div>
44
+ </div>
45
+ </div>
46
+
47
+ <!-- Right Panel: Detail View -->
48
+ <div class="panel">
49
+ <div class="panel-header">Details</div>
50
+ <div class="panel-content">
51
+ <div class="detail-content" id="detailView">
52
+ <div class="detail-empty">Select an event to view details</div>
53
+ </div>
54
+ </div>
55
+ </div>
56
+ </div>
57
+
58
+ <script>
59
+ // API base URL
60
+ const API_BASE = '/v1';
61
+
62
+ // State
63
+ let currentRunId = null;
64
+ let currentEventId = null;
65
+ let runs = [];
66
+
67
+ // DOM elements
68
+ const runList = document.getElementById('runList');
69
+ const timeline = document.getElementById('timeline');
70
+ const detailView = document.getElementById('detailView');
71
+ const searchInput = document.getElementById('searchInput');
72
+ const statusFilter = document.getElementById('statusFilter');
73
+ const eventTypeFilter = document.getElementById('eventTypeFilter');
74
+ const themeToggle = document.getElementById('themeToggle');
75
+
76
+ // Load runs on page load
77
+ window.addEventListener('load', () => {
78
+ initTheme();
79
+ loadRuns();
80
+ });
81
+
82
+ // Search input debounce
83
+ let searchTimeout;
84
+ searchInput.addEventListener('input', (e) => {
85
+ clearTimeout(searchTimeout);
86
+ searchTimeout = setTimeout(() => filterRuns(), 300);
87
+ });
88
+
89
+ // Filter change handlers
90
+ statusFilter.addEventListener('change', filterRuns);
91
+ eventTypeFilter.addEventListener('change', () => {
92
+ if (currentRunId) {
93
+ loadTimeline(currentRunId);
94
+ }
95
+ });
96
+
97
+ async function loadRuns() {
98
+ try {
99
+ const response = await fetch(`${API_BASE}/runs?limit=100`);
100
+ const data = await response.json();
101
+ runs = data.runs || [];
102
+ renderRuns(runs);
103
+ } catch (error) {
104
+ runList.innerHTML = '<li class="error">Failed to load runs</li>';
105
+ console.error('Error loading runs:', error);
106
+ }
107
+ }
108
+
109
+ function renderRuns(runsToRender) {
110
+ if (runsToRender.length === 0) {
111
+ runList.innerHTML = '<li class="loading" style="padding: 40px;">No runs found</li>';
112
+ return;
113
+ }
114
+
115
+ runList.innerHTML = runsToRender.map(run => `
116
+ <li class="run-item ${currentRunId === run.id ? 'active' : ''}"
117
+ data-run-id="${run.id}"
118
+ onclick="selectRun('${run.id}')">
119
+ <div class="run-name">${escapeHtml(run.name || 'Unnamed Run')}</div>
120
+ <div class="run-meta">
121
+ ${formatTimestamp(run.started_at)}
122
+ <span class="run-status ${run.status}">${run.status}</span>
123
+ </div>
124
+ </li>
125
+ `).join('');
126
+ }
127
+
128
+ function filterRuns() {
129
+ const searchTerm = searchInput.value.toLowerCase();
130
+ const status = statusFilter.value;
131
+
132
+ const filtered = runs.filter(run => {
133
+ const matchesSearch = !searchTerm ||
134
+ (run.name && run.name.toLowerCase().includes(searchTerm));
135
+ const matchesStatus = !status || run.status === status;
136
+ return matchesSearch && matchesStatus;
137
+ });
138
+
139
+ renderRuns(filtered);
140
+ }
141
+
142
+ async function selectRun(runId) {
143
+ currentRunId = runId;
144
+
145
+ // Update UI
146
+ document.querySelectorAll('.run-item').forEach(item => {
147
+ item.classList.remove('active');
148
+ if (item.dataset.runId === runId) {
149
+ item.classList.add('active');
150
+ }
151
+ });
152
+
153
+ // Clear detail view
154
+ detailView.innerHTML = '<div class="detail-empty">Loading...</div>';
155
+
156
+ // Load timeline
157
+ await loadTimeline(runId);
158
+ }
159
+
160
+ async function loadTimeline(runId) {
161
+ try {
162
+ timeline.innerHTML = '<div class="loading">Loading timeline...</div>';
163
+
164
+ const response = await fetch(`${API_BASE}/runs/${runId}/timeline`);
165
+ const data = await response.json();
166
+ const events = data.events || [];
167
+
168
+ renderTimeline(events);
169
+ } catch (error) {
170
+ timeline.innerHTML = '<div class="error">Failed to load timeline</div>';
171
+ console.error('Error loading timeline:', error);
172
+ }
173
+ }
174
+
175
+ function renderTimeline(events) {
176
+ if (events.length === 0) {
177
+ timeline.innerHTML = '<div class="loading">No events in this run</div>';
178
+ return;
179
+ }
180
+
181
+ const eventTypeFilter = document.getElementById('eventTypeFilter').value;
182
+ const filteredEvents = eventTypeFilter
183
+ ? events.filter(e => e.type === eventTypeFilter)
184
+ : events;
185
+
186
+ if (filteredEvents.length === 0) {
187
+ timeline.innerHTML = '<div class="loading">No events match filter</div>';
188
+ return;
189
+ }
190
+
191
+ timeline.innerHTML = `
192
+ <div class="event-connector"></div>
193
+ ${filteredEvents.map(event => `
194
+ <div class="timeline-event"
195
+ onclick="showEventDetail('${event.id}')"
196
+ data-event-id="${event.id}">
197
+ <div class="event-icon ${event.type}">
198
+ ${getEventIcon(event.type)}
199
+ </div>
200
+ <div class="event-content">
201
+ <div class="event-type">${formatEventType(event.type)}</div>
202
+ <div class="event-timestamp">${formatTimestamp(event.timestamp)}</div>
203
+ <div class="event-summary">${getEventSummary(event)}</div>
204
+ </div>
205
+ </div>
206
+ `).join('')}
207
+ `;
208
+ }
209
+
210
+ async function showEventDetail(eventId) {
211
+ currentEventId = eventId;
212
+
213
+ try {
214
+ const response = await fetch(`${API_BASE}/runs/${currentRunId}/steps/${eventId}/data`);
215
+ const data = await response.json();
216
+
217
+ renderEventDetail(data.data);
218
+ } catch (error) {
219
+ detailView.innerHTML = '<div class="error">Failed to load event details</div>';
220
+ console.error('Error loading event details:', error);
221
+ }
222
+ }
223
+
224
+ function renderEventDetail(event) {
225
+ if (!event) {
226
+ detailView.innerHTML = '<div class="detail-empty">No event data</div>';
227
+ return;
228
+ }
229
+
230
+ const sections = [];
231
+ const richBlocks = [];
232
+
233
+ // Basic info
234
+ sections.push({
235
+ label: 'Event ID',
236
+ value: event.event_id || 'N/A'
237
+ });
238
+
239
+ sections.push({
240
+ label: 'Type',
241
+ value: formatEventType(event.type)
242
+ });
243
+
244
+ sections.push({
245
+ label: 'Timestamp',
246
+ value: formatTimestamp(event.timestamp_ms)
247
+ });
248
+
249
+ sections.push({
250
+ label: 'Status',
251
+ value: event.status || 'N/A'
252
+ });
253
+
254
+ if (event.duration_ms !== undefined) {
255
+ sections.push({
256
+ label: 'Duration',
257
+ value: `${event.duration_ms}ms`
258
+ });
259
+ }
260
+
261
+ // Type-specific details
262
+ if (event.type === 'llm_call') {
263
+ if (event.model) {
264
+ sections.push({ label: 'Model', value: event.model });
265
+ }
266
+ if (event.prompt) {
267
+ const parsed = parseMaybeJson(event.prompt);
268
+ if (parsed && Array.isArray(parsed)) {
269
+ richBlocks.push({
270
+ label: 'Prompt',
271
+ html: renderChatMessages(parsed)
272
+ });
273
+ } else {
274
+ sections.push({ label: 'Prompt', value: event.prompt });
275
+ }
276
+ }
277
+ if (event.response) {
278
+ sections.push({ label: 'Response', value: event.response });
279
+ }
280
+ if (event.total_tokens) {
281
+ sections.push({ label: 'Tokens', value: event.total_tokens.toString() });
282
+ }
283
+ } else if (event.type === 'tool_call') {
284
+ if (event.tool_name) {
285
+ sections.push({ label: 'Tool', value: event.tool_name });
286
+ }
287
+ if (event.tool_args) {
288
+ sections.push({
289
+ label: 'Arguments',
290
+ value: JSON.stringify(event.tool_args, null, 2)
291
+ });
292
+ }
293
+ if (event.tool_result) {
294
+ sections.push({
295
+ label: 'Result',
296
+ value: JSON.stringify(event.tool_result, null, 2)
297
+ });
298
+ }
299
+ } else if (event.type === 'memory_read' || event.type === 'memory_write') {
300
+ if (event.memory_key) {
301
+ sections.push({ label: 'Key', value: event.memory_key });
302
+ }
303
+ if (event.memory_value) {
304
+ sections.push({
305
+ label: 'Value',
306
+ value: JSON.stringify(event.memory_value, null, 2)
307
+ });
308
+ }
309
+ } else if (event.type === 'error') {
310
+ if (event.error_type) {
311
+ sections.push({ label: 'Error Type', value: event.error_type });
312
+ }
313
+ if (event.error_message) {
314
+ sections.push({ label: 'Message', value: event.error_message });
315
+ }
316
+ } else if (event.type === 'final_answer') {
317
+ if (event.answer) {
318
+ sections.push({ label: 'Answer', value: event.answer });
319
+ }
320
+ }
321
+
322
+ // Metadata
323
+ if (event.metadata && Object.keys(event.metadata).length > 0) {
324
+ sections.push({
325
+ label: 'Metadata',
326
+ value: JSON.stringify(event.metadata, null, 2)
327
+ });
328
+ }
329
+
330
+ // Render sections
331
+ const sectionHtml = sections.map(section => `
332
+ <div class="detail-section">
333
+ <div class="detail-label">${escapeHtml(section.label)}</div>
334
+ <div class="detail-value">${escapeHtml(section.value)}</div>
335
+ </div>
336
+ `).join('');
337
+
338
+ const richHtml = richBlocks.map(block => `
339
+ <div class="detail-section">
340
+ <div class="detail-label">${escapeHtml(block.label)}</div>
341
+ <div class="detail-chat">${block.html}</div>
342
+ </div>
343
+ `).join('');
344
+
345
+ detailView.innerHTML = richHtml + sectionHtml;
346
+ }
347
+
348
+ function getEventIcon(type) {
349
+ const icons = {
350
+ 'llm_call': '🤖',
351
+ 'tool_call': '🔧',
352
+ 'memory_read': '📖',
353
+ 'memory_write': '✍️',
354
+ 'error': '❌',
355
+ 'final_answer': '✅',
356
+ 'run_start': '▶️'
357
+ };
358
+ return icons[type] || '📌';
359
+ }
360
+
361
+ function formatEventType(type) {
362
+ return type.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase());
363
+ }
364
+
365
+ function getEventSummary(event) {
366
+ switch (event.type) {
367
+ case 'llm_call':
368
+ return event.model ? `Model: ${event.model}` : 'LLM Call';
369
+ case 'tool_call':
370
+ return event.tool_name ? `Tool: ${event.tool_name}` : 'Tool Call';
371
+ case 'memory_read':
372
+ return event.memory_key ? `Read: ${event.memory_key}` : 'Memory Read';
373
+ case 'memory_write':
374
+ return event.memory_key ? `Write: ${event.memory_key}` : 'Memory Write';
375
+ case 'error':
376
+ return event.error_message || 'Error occurred';
377
+ case 'final_answer':
378
+ return event.answer ? event.answer.substring(0, 50) + '...' : 'Final answer';
379
+ default:
380
+ return event.name || event.type;
381
+ }
382
+ }
383
+
384
+ function formatTimestamp(ms) {
385
+ const date = new Date(ms);
386
+ return date.toLocaleString();
387
+ }
388
+
389
+ function escapeHtml(text) {
390
+ const div = document.createElement('div');
391
+ div.textContent = text;
392
+ return div.innerHTML;
393
+ }
394
+
395
+ function initTheme() {
396
+ const saved = localStorage.getItem('agent_inspector_theme') || 'auto';
397
+ applyTheme(saved);
398
+ themeToggle.addEventListener('click', cycleTheme);
399
+ }
400
+
401
+ function cycleTheme() {
402
+ const current = localStorage.getItem('agent_inspector_theme') || 'auto';
403
+ const next = current === 'auto' ? 'light' : current === 'light' ? 'dark' : 'auto';
404
+ applyTheme(next);
405
+ }
406
+
407
+ function applyTheme(theme) {
408
+ const root = document.documentElement;
409
+ root.classList.remove('theme-light', 'theme-dark');
410
+ if (theme === 'light') {
411
+ root.classList.add('theme-light');
412
+ } else if (theme === 'dark') {
413
+ root.classList.add('theme-dark');
414
+ }
415
+ localStorage.setItem('agent_inspector_theme', theme);
416
+ const label = theme === 'auto'
417
+ ? 'Theme: Auto'
418
+ : `Theme: ${theme[0].toUpperCase()}${theme.slice(1)}`;
419
+ themeToggle.textContent = label;
420
+ }
421
+
422
+ function parseMaybeJson(text) {
423
+ try {
424
+ return JSON.parse(text);
425
+ } catch (e) {
426
+ return null;
427
+ }
428
+ }
429
+
430
+ function renderChatMessages(messages) {
431
+ return messages.map(msg => `
432
+ <div class="chat-row ${escapeHtml(msg.role || 'unknown')}">
433
+ <div class="chat-role">${escapeHtml(msg.role || 'unknown')}</div>
434
+ <div class="chat-content">${escapeHtml(msg.content || '')}</div>
435
+ </div>
436
+ `).join('');
437
+ }
438
+ </script>
439
+ <script src="/ui/static/app.js" defer></script>
440
+ </body>
441
+ </html>