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