RouteKitAI 0.1.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.
Files changed (64) hide show
  1. routekitai/__init__.py +53 -0
  2. routekitai/cli/__init__.py +18 -0
  3. routekitai/cli/main.py +40 -0
  4. routekitai/cli/replay.py +80 -0
  5. routekitai/cli/run.py +95 -0
  6. routekitai/cli/serve.py +966 -0
  7. routekitai/cli/test_agent.py +178 -0
  8. routekitai/cli/trace.py +209 -0
  9. routekitai/cli/trace_analyze.py +120 -0
  10. routekitai/cli/trace_search.py +126 -0
  11. routekitai/core/__init__.py +58 -0
  12. routekitai/core/agent.py +325 -0
  13. routekitai/core/errors.py +49 -0
  14. routekitai/core/hooks.py +174 -0
  15. routekitai/core/memory.py +54 -0
  16. routekitai/core/message.py +132 -0
  17. routekitai/core/model.py +91 -0
  18. routekitai/core/policies.py +373 -0
  19. routekitai/core/policy.py +85 -0
  20. routekitai/core/policy_adapter.py +133 -0
  21. routekitai/core/runtime.py +1403 -0
  22. routekitai/core/tool.py +148 -0
  23. routekitai/core/tools.py +180 -0
  24. routekitai/evals/__init__.py +13 -0
  25. routekitai/evals/dataset.py +75 -0
  26. routekitai/evals/metrics.py +101 -0
  27. routekitai/evals/runner.py +184 -0
  28. routekitai/graphs/__init__.py +12 -0
  29. routekitai/graphs/executors.py +457 -0
  30. routekitai/graphs/graph.py +164 -0
  31. routekitai/memory/__init__.py +13 -0
  32. routekitai/memory/episodic.py +242 -0
  33. routekitai/memory/kv.py +34 -0
  34. routekitai/memory/retrieval.py +192 -0
  35. routekitai/memory/vector.py +700 -0
  36. routekitai/memory/working.py +66 -0
  37. routekitai/message.py +29 -0
  38. routekitai/model.py +48 -0
  39. routekitai/observability/__init__.py +21 -0
  40. routekitai/observability/analyzer.py +314 -0
  41. routekitai/observability/exporters/__init__.py +10 -0
  42. routekitai/observability/exporters/base.py +30 -0
  43. routekitai/observability/exporters/jsonl.py +81 -0
  44. routekitai/observability/exporters/otel.py +119 -0
  45. routekitai/observability/spans.py +111 -0
  46. routekitai/observability/streaming.py +117 -0
  47. routekitai/observability/trace.py +144 -0
  48. routekitai/providers/__init__.py +9 -0
  49. routekitai/providers/anthropic.py +227 -0
  50. routekitai/providers/azure_openai.py +243 -0
  51. routekitai/providers/local.py +196 -0
  52. routekitai/providers/openai.py +321 -0
  53. routekitai/py.typed +0 -0
  54. routekitai/sandbox/__init__.py +12 -0
  55. routekitai/sandbox/filesystem.py +131 -0
  56. routekitai/sandbox/network.py +142 -0
  57. routekitai/sandbox/permissions.py +70 -0
  58. routekitai/tool.py +33 -0
  59. routekitai-0.1.0.dist-info/METADATA +328 -0
  60. routekitai-0.1.0.dist-info/RECORD +64 -0
  61. routekitai-0.1.0.dist-info/WHEEL +5 -0
  62. routekitai-0.1.0.dist-info/entry_points.txt +2 -0
  63. routekitai-0.1.0.dist-info/licenses/LICENSE +21 -0
  64. routekitai-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,966 @@
1
+ """CLI command for starting the trace visualization web server."""
2
+
3
+ import os
4
+ from collections.abc import AsyncIterator
5
+ from pathlib import Path
6
+
7
+ try:
8
+ import typer
9
+ from fastapi import FastAPI, HTTPException, WebSocket, WebSocketDisconnect
10
+ from fastapi.middleware.cors import CORSMiddleware
11
+ from fastapi.responses import HTMLResponse, JSONResponse, StreamingResponse
12
+ except ImportError as e:
13
+ raise ImportError(
14
+ "Web UI dependencies not installed. Install with: pip install 'routekit[ui]'"
15
+ ) from e
16
+
17
+ from routekitai.observability.analyzer import TraceAnalyzer
18
+ from routekitai.observability.exporters.jsonl import JSONLExporter
19
+ from routekitai.observability.streaming import get_broadcaster
20
+
21
+ app = FastAPI(title="routkitai Trace Viewer", version="0.1.0")
22
+
23
+ # Enable CORS for local development
24
+ app.add_middleware(
25
+ CORSMiddleware,
26
+ allow_origins=["*"],
27
+ allow_credentials=True,
28
+ allow_methods=["*"],
29
+ allow_headers=["*"],
30
+ )
31
+
32
+
33
+ @app.get("/api/traces")
34
+ async def list_traces() -> JSONResponse:
35
+ """List all available traces with summary metrics.
36
+
37
+ Returns:
38
+ JSON response with list of traces and their metrics
39
+ """
40
+ trace_dir = os.environ.get("ROUTEKIT_TRACE_DIR", ".routekit/traces")
41
+ trace_path = Path(trace_dir)
42
+ if not trace_path.exists():
43
+ return JSONResponse({"traces": []})
44
+
45
+ traces = []
46
+ exporter = JSONLExporter(output_dir=trace_path)
47
+ analyzer = TraceAnalyzer()
48
+
49
+ for trace_file in sorted(
50
+ trace_path.glob("*.jsonl"), key=lambda p: p.stat().st_mtime, reverse=True
51
+ ):
52
+ trace_id = trace_file.stem
53
+ try:
54
+ trace = await exporter.load(trace_id)
55
+ if trace:
56
+ metrics = analyzer.analyze(trace)
57
+ traces.append(
58
+ {
59
+ "trace_id": trace_id,
60
+ "total_events": metrics.total_events,
61
+ "duration_ms": metrics.total_duration_ms,
62
+ "model_calls": metrics.model_calls,
63
+ "tool_calls": metrics.tool_calls,
64
+ "errors": metrics.errors,
65
+ "total_tokens": metrics.total_tokens,
66
+ "error_rate": metrics.error_rate,
67
+ }
68
+ )
69
+ except Exception:
70
+ # Skip corrupted traces
71
+ continue
72
+
73
+ return JSONResponse({"traces": traces})
74
+
75
+
76
+ @app.get("/api/traces/{trace_id}")
77
+ async def get_trace(trace_id: str) -> JSONResponse:
78
+ """Get full trace data including metrics, timeline, and steps.
79
+
80
+ Args:
81
+ trace_id: Trace ID to retrieve
82
+
83
+ Returns:
84
+ JSON response with complete trace data
85
+ """
86
+ try:
87
+ trace_dir = os.environ.get("ROUTEKIT_TRACE_DIR", ".routekit/traces")
88
+ trace_path = Path(trace_dir)
89
+ if not trace_path.exists():
90
+ raise HTTPException(status_code=404, detail="Trace directory not found")
91
+
92
+ exporter = JSONLExporter(output_dir=trace_path)
93
+ trace = await exporter.load(trace_id)
94
+
95
+ if not trace:
96
+ raise HTTPException(status_code=404, detail=f"Trace '{trace_id}' not found")
97
+
98
+ analyzer = TraceAnalyzer()
99
+ metrics = analyzer.analyze(trace)
100
+ timeline = analyzer.get_timeline(trace)
101
+ steps = analyzer.get_step_sequence(trace)
102
+
103
+ # Convert timeline entries to serializable format
104
+ timeline_data = []
105
+ for entry in timeline:
106
+ try:
107
+ event_obj = entry.get("event")
108
+ if event_obj is None:
109
+ continue
110
+ # Handle both TraceEvent objects and dicts
111
+ if hasattr(event_obj, "model_dump"):
112
+ event_data = event_obj.model_dump(mode="json")
113
+ else:
114
+ event_data = event_obj
115
+ timeline_data.append(
116
+ {
117
+ "event": event_data,
118
+ "relative_time_ms": entry.get("relative_time_ms", 0.0),
119
+ "duration_ms": entry.get("duration_ms", 0.0),
120
+ "index": entry.get("index", 0),
121
+ }
122
+ )
123
+ except Exception:
124
+ # Skip problematic entries
125
+ continue
126
+
127
+ return JSONResponse(
128
+ {
129
+ "trace": trace.model_dump(mode="json"),
130
+ "metrics": metrics.model_dump(mode="json"),
131
+ "timeline": timeline_data,
132
+ "steps": steps,
133
+ }
134
+ )
135
+ except HTTPException:
136
+ raise
137
+ except Exception as e:
138
+ raise HTTPException(status_code=500, detail=f"Error loading trace: {str(e)}") from e
139
+
140
+
141
+ @app.get("/api/traces/{trace_id}/events")
142
+ async def get_trace_events(
143
+ trace_id: str,
144
+ event_type: str | None = None,
145
+ ) -> JSONResponse:
146
+ """Get events from a trace, optionally filtered by type.
147
+
148
+ Args:
149
+ trace_id: Trace ID
150
+ event_type: Optional event type filter
151
+
152
+ Returns:
153
+ JSON response with filtered events
154
+ """
155
+ trace_dir = os.environ.get("ROUTEKIT_TRACE_DIR", ".routekit/traces")
156
+ trace_path = Path(trace_dir)
157
+ exporter = JSONLExporter(output_dir=trace_path)
158
+ trace = await exporter.load(trace_id)
159
+
160
+ if not trace:
161
+ raise HTTPException(status_code=404, detail=f"Trace '{trace_id}' not found")
162
+
163
+ analyzer = TraceAnalyzer()
164
+ events = analyzer.query(trace, event_type=event_type)
165
+
166
+ return JSONResponse({"events": [e.model_dump() for e in events]})
167
+
168
+
169
+ @app.get("/api/traces/{trace_id}/search")
170
+ async def search_trace(
171
+ trace_id: str,
172
+ query: str,
173
+ ) -> JSONResponse:
174
+ """Search events in a trace.
175
+
176
+ Args:
177
+ trace_id: Trace ID
178
+ query: Search query
179
+
180
+ Returns:
181
+ JSON response with matching events
182
+ """
183
+ trace_dir = os.environ.get("ROUTEKIT_TRACE_DIR", ".routekit/traces")
184
+ trace_path = Path(trace_dir)
185
+ exporter = JSONLExporter(output_dir=trace_path)
186
+ trace = await exporter.load(trace_id)
187
+
188
+ if not trace:
189
+ raise HTTPException(status_code=404, detail=f"Trace '{trace_id}' not found")
190
+
191
+ analyzer = TraceAnalyzer()
192
+ results = analyzer.search(trace, query)
193
+
194
+ return JSONResponse({"results": [e.model_dump() for e in results]})
195
+
196
+
197
+ @app.websocket("/ws/traces/{trace_id}")
198
+ async def websocket_trace_stream(websocket: WebSocket, trace_id: str) -> None:
199
+ """WebSocket endpoint for real-time trace event streaming.
200
+
201
+ Args:
202
+ websocket: WebSocket connection
203
+ trace_id: Trace ID to stream (use '*' for all traces)
204
+ """
205
+ await websocket.accept()
206
+ broadcaster = get_broadcaster()
207
+ queue = await broadcaster.subscribe()
208
+
209
+ try:
210
+ while True:
211
+ try:
212
+ # Get event from queue
213
+ event = await queue.get()
214
+ queue.task_done()
215
+
216
+ # Filter by trace_id if not '*'
217
+ if trace_id != "*" and event.data.get("trace_id") != trace_id:
218
+ continue
219
+
220
+ # Send event as JSON
221
+ await websocket.send_json(
222
+ {
223
+ "type": event.type,
224
+ "timestamp": event.timestamp,
225
+ "data": event.data,
226
+ }
227
+ )
228
+ except WebSocketDisconnect:
229
+ break
230
+ except Exception as e:
231
+ await websocket.send_json({"error": str(e)})
232
+ break
233
+ finally:
234
+ await broadcaster.unsubscribe(queue)
235
+
236
+
237
+ @app.get("/api/traces/{trace_id}/stream")
238
+ async def sse_trace_stream(trace_id: str) -> StreamingResponse:
239
+ """Server-Sent Events (SSE) endpoint for real-time trace event streaming.
240
+
241
+ Args:
242
+ trace_id: Trace ID to stream (use '*' for all traces)
243
+
244
+ Returns:
245
+ StreamingResponse with SSE-formatted events
246
+ """
247
+ broadcaster = get_broadcaster()
248
+
249
+ async def event_generator() -> AsyncIterator[str]:
250
+ queue = await broadcaster.subscribe()
251
+ try:
252
+ async for event_data in broadcaster.stream_events(
253
+ queue, trace_id if trace_id != "*" else None
254
+ ):
255
+ yield event_data
256
+ finally:
257
+ await broadcaster.unsubscribe(queue)
258
+
259
+ return StreamingResponse(
260
+ event_generator(),
261
+ media_type="text/event-stream",
262
+ headers={
263
+ "Cache-Control": "no-cache",
264
+ "Connection": "keep-alive",
265
+ "X-Accel-Buffering": "no",
266
+ },
267
+ )
268
+
269
+
270
+ @app.get("/")
271
+ async def index() -> HTMLResponse:
272
+ """Serve the trace visualization dashboard."""
273
+ html_content = _get_dashboard_html()
274
+ return HTMLResponse(html_content)
275
+
276
+
277
+ def _get_dashboard_html() -> str:
278
+ """Get the HTML dashboard content."""
279
+ return """
280
+ <!DOCTYPE html>
281
+ <html lang="en">
282
+ <head>
283
+ <meta charset="UTF-8">
284
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
285
+ <title>routkitai Trace Viewer</title>
286
+ <script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
287
+ <style>
288
+ * {
289
+ margin: 0;
290
+ padding: 0;
291
+ box-sizing: border-box;
292
+ }
293
+
294
+ body {
295
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
296
+ background: #f5f5f5;
297
+ color: #333;
298
+ line-height: 1.6;
299
+ }
300
+
301
+ .container {
302
+ max-width: 1400px;
303
+ margin: 0 auto;
304
+ padding: 20px;
305
+ }
306
+
307
+ header {
308
+ background: white;
309
+ padding: 20px;
310
+ border-radius: 8px;
311
+ margin-bottom: 20px;
312
+ box-shadow: 0 2px 4px rgba(0,0,0,0.1);
313
+ }
314
+
315
+ h1 {
316
+ color: #2563eb;
317
+ margin-bottom: 10px;
318
+ }
319
+
320
+ .trace-list {
321
+ display: grid;
322
+ grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
323
+ gap: 15px;
324
+ margin-bottom: 30px;
325
+ }
326
+
327
+ .trace-card {
328
+ background: white;
329
+ border: 1px solid #e5e7eb;
330
+ border-radius: 8px;
331
+ padding: 15px;
332
+ cursor: pointer;
333
+ transition: all 0.2s;
334
+ box-shadow: 0 1px 3px rgba(0,0,0,0.1);
335
+ }
336
+
337
+ .trace-card:hover {
338
+ transform: translateY(-2px);
339
+ box-shadow: 0 4px 6px rgba(0,0,0,0.1);
340
+ border-color: #2563eb;
341
+ }
342
+
343
+ .trace-card.selected {
344
+ border-color: #2563eb;
345
+ background: #eff6ff;
346
+ }
347
+
348
+ .trace-card h3 {
349
+ color: #1f2937;
350
+ margin-bottom: 10px;
351
+ font-size: 14px;
352
+ font-weight: 600;
353
+ word-break: break-all;
354
+ }
355
+
356
+ .trace-card .metrics {
357
+ display: grid;
358
+ grid-template-columns: repeat(2, 1fr);
359
+ gap: 8px;
360
+ font-size: 12px;
361
+ }
362
+
363
+ .trace-card .metric {
364
+ display: flex;
365
+ justify-content: space-between;
366
+ }
367
+
368
+ .trace-card .metric-label {
369
+ color: #6b7280;
370
+ }
371
+
372
+ .trace-card .metric-value {
373
+ font-weight: 600;
374
+ color: #1f2937;
375
+ }
376
+
377
+ .trace-detail {
378
+ background: white;
379
+ border-radius: 8px;
380
+ padding: 20px;
381
+ box-shadow: 0 2px 4px rgba(0,0,0,0.1);
382
+ display: none;
383
+ }
384
+
385
+ .trace-detail.active {
386
+ display: block;
387
+ }
388
+
389
+ .detail-header {
390
+ display: flex;
391
+ justify-content: space-between;
392
+ align-items: center;
393
+ margin-bottom: 20px;
394
+ padding-bottom: 15px;
395
+ border-bottom: 2px solid #e5e7eb;
396
+ }
397
+
398
+ .detail-header h2 {
399
+ color: #1f2937;
400
+ }
401
+
402
+ .close-btn {
403
+ background: #ef4444;
404
+ color: white;
405
+ border: none;
406
+ padding: 8px 16px;
407
+ border-radius: 4px;
408
+ cursor: pointer;
409
+ font-size: 14px;
410
+ }
411
+
412
+ .close-btn:hover {
413
+ background: #dc2626;
414
+ }
415
+
416
+ .metrics-grid {
417
+ display: grid;
418
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
419
+ gap: 15px;
420
+ margin-bottom: 30px;
421
+ }
422
+
423
+ .metric-card {
424
+ background: #f9fafb;
425
+ border: 1px solid #e5e7eb;
426
+ border-radius: 6px;
427
+ padding: 15px;
428
+ }
429
+
430
+ .metric-card h4 {
431
+ color: #6b7280;
432
+ font-size: 12px;
433
+ text-transform: uppercase;
434
+ margin-bottom: 8px;
435
+ }
436
+
437
+ .metric-card .value {
438
+ font-size: 24px;
439
+ font-weight: 700;
440
+ color: #1f2937;
441
+ }
442
+
443
+ .metric-card .unit {
444
+ font-size: 14px;
445
+ color: #6b7280;
446
+ margin-left: 4px;
447
+ }
448
+
449
+ .chart-container {
450
+ background: white;
451
+ border: 1px solid #e5e7eb;
452
+ border-radius: 6px;
453
+ padding: 20px;
454
+ margin-bottom: 20px;
455
+ }
456
+
457
+ .chart-container h3 {
458
+ margin-bottom: 15px;
459
+ color: #1f2937;
460
+ }
461
+
462
+ .timeline-container {
463
+ margin-top: 20px;
464
+ }
465
+
466
+ .timeline-event {
467
+ display: flex;
468
+ align-items: center;
469
+ padding: 10px;
470
+ margin: 5px 0;
471
+ background: #f9fafb;
472
+ border-left: 4px solid #2563eb;
473
+ border-radius: 4px;
474
+ cursor: pointer;
475
+ }
476
+
477
+ .timeline-event:hover {
478
+ background: #f3f4f6;
479
+ }
480
+
481
+ .timeline-event.error {
482
+ border-left-color: #ef4444;
483
+ }
484
+
485
+ .timeline-event.model {
486
+ border-left-color: #10b981;
487
+ }
488
+
489
+ .timeline-event.tool {
490
+ border-left-color: #f59e0b;
491
+ }
492
+
493
+ .event-time {
494
+ min-width: 100px;
495
+ font-size: 12px;
496
+ color: #6b7280;
497
+ }
498
+
499
+ .event-type {
500
+ min-width: 150px;
501
+ font-weight: 600;
502
+ color: #1f2937;
503
+ }
504
+
505
+ .event-details {
506
+ flex: 1;
507
+ font-size: 12px;
508
+ color: #6b7280;
509
+ overflow: hidden;
510
+ text-overflow: ellipsis;
511
+ white-space: nowrap;
512
+ }
513
+
514
+ .loading {
515
+ text-align: center;
516
+ padding: 40px;
517
+ color: #6b7280;
518
+ }
519
+
520
+ .error {
521
+ background: #fef2f2;
522
+ border: 1px solid #fecaca;
523
+ color: #991b1b;
524
+ padding: 15px;
525
+ border-radius: 6px;
526
+ margin: 20px 0;
527
+ }
528
+
529
+ .steps-container {
530
+ margin-top: 20px;
531
+ }
532
+
533
+ .step-card {
534
+ background: #f9fafb;
535
+ border: 1px solid #e5e7eb;
536
+ border-radius: 6px;
537
+ padding: 15px;
538
+ margin-bottom: 15px;
539
+ }
540
+
541
+ .step-header {
542
+ display: flex;
543
+ justify-content: space-between;
544
+ align-items: center;
545
+ margin-bottom: 10px;
546
+ }
547
+
548
+ .step-id {
549
+ font-weight: 600;
550
+ color: #1f2937;
551
+ }
552
+
553
+ .step-duration {
554
+ font-size: 12px;
555
+ color: #6b7280;
556
+ }
557
+
558
+ .step-error {
559
+ background: #fef2f2;
560
+ border: 1px solid #fecaca;
561
+ color: #991b1b;
562
+ padding: 10px;
563
+ border-radius: 4px;
564
+ margin-top: 10px;
565
+ font-size: 12px;
566
+ }
567
+ </style>
568
+ </head>
569
+ <body>
570
+ <div class="container">
571
+ <header>
572
+ <h1>šŸ” routkitai Trace Viewer</h1>
573
+ <p>Visualize and analyze agent execution traces</p>
574
+ </header>
575
+
576
+ <div id="trace-list" class="trace-list">
577
+ <div class="loading">Loading traces...</div>
578
+ </div>
579
+
580
+ <div id="trace-detail" class="trace-detail">
581
+ <div class="detail-header">
582
+ <h2 id="detail-title">Trace Details</h2>
583
+ <button class="close-btn" onclick="closeDetail()">Close</button>
584
+ </div>
585
+
586
+ <div id="detail-content">
587
+ <div class="loading">Loading trace data...</div>
588
+ </div>
589
+ </div>
590
+ </div>
591
+
592
+ <script>
593
+ let currentTraceId = null;
594
+ let charts = {};
595
+
596
+ // Load traces on page load
597
+ async function loadTraces() {
598
+ try {
599
+ const response = await fetch('/api/traces');
600
+ if (!response.ok) {
601
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
602
+ }
603
+ const data = await response.json();
604
+ renderTraceList(data.traces || []);
605
+ } catch (error) {
606
+ document.getElementById('trace-list').innerHTML =
607
+ '<div class="error">Error loading traces: ' + error.message + '</div>';
608
+ }
609
+ }
610
+
611
+ function renderTraceList(traces) {
612
+ const container = document.getElementById('trace-list');
613
+
614
+ if (traces.length === 0) {
615
+ container.innerHTML = '<div class="loading">No traces found</div>';
616
+ return;
617
+ }
618
+
619
+ container.innerHTML = traces.map(trace => `
620
+ <div class="trace-card" onclick="loadTrace('${trace.trace_id}')">
621
+ <h3>${trace.trace_id}</h3>
622
+ <div class="metrics">
623
+ <div class="metric">
624
+ <span class="metric-label">Events:</span>
625
+ <span class="metric-value">${trace.total_events}</span>
626
+ </div>
627
+ <div class="metric">
628
+ <span class="metric-label">Duration:</span>
629
+ <span class="metric-value">${trace.duration_ms.toFixed(0)}ms</span>
630
+ </div>
631
+ <div class="metric">
632
+ <span class="metric-label">Model Calls:</span>
633
+ <span class="metric-value">${trace.model_calls}</span>
634
+ </div>
635
+ <div class="metric">
636
+ <span class="metric-label">Tool Calls:</span>
637
+ <span class="metric-value">${trace.tool_calls}</span>
638
+ </div>
639
+ ${trace.errors > 0 ? `
640
+ <div class="metric">
641
+ <span class="metric-label">Errors:</span>
642
+ <span class="metric-value" style="color: #ef4444;">${trace.errors}</span>
643
+ </div>
644
+ ` : ''}
645
+ ${trace.total_tokens > 0 ? `
646
+ <div class="metric">
647
+ <span class="metric-label">Tokens:</span>
648
+ <span class="metric-value">${trace.total_tokens}</span>
649
+ </div>
650
+ ` : ''}
651
+ </div>
652
+ </div>
653
+ `).join('');
654
+ }
655
+
656
+ async function loadTrace(traceId) {
657
+ currentTraceId = traceId;
658
+
659
+ // Update selected card
660
+ document.querySelectorAll('.trace-card').forEach(card => {
661
+ card.classList.remove('selected');
662
+ if (card.querySelector('h3').textContent === traceId) {
663
+ card.classList.add('selected');
664
+ }
665
+ });
666
+
667
+ // Show detail panel
668
+ document.getElementById('trace-detail').classList.add('active');
669
+ document.getElementById('detail-content').innerHTML = '<div class="loading">Loading...</div>';
670
+
671
+ try {
672
+ const response = await fetch(`/api/traces/${traceId}`);
673
+ if (!response.ok) {
674
+ const errorText = await response.text();
675
+ let errorMessage = `HTTP ${response.status}: ${response.statusText}`;
676
+ try {
677
+ const errorJson = JSON.parse(errorText);
678
+ errorMessage = errorJson.detail || errorMessage;
679
+ } catch {
680
+ // If not JSON, use the text as-is (might be HTML)
681
+ if (errorText.length < 200) {
682
+ errorMessage = errorText;
683
+ }
684
+ }
685
+ throw new Error(errorMessage);
686
+ }
687
+ const data = await response.json();
688
+ renderTraceDetail(data);
689
+ } catch (error) {
690
+ document.getElementById('detail-content').innerHTML =
691
+ '<div class="error">Error loading trace: ' + error.message + '</div>';
692
+ }
693
+ }
694
+
695
+ function renderTraceDetail(data) {
696
+ const metrics = data.metrics;
697
+ const timeline = data.timeline;
698
+ const steps = data.steps;
699
+
700
+ let html = `
701
+ <div class="metrics-grid">
702
+ <div class="metric-card">
703
+ <h4>Total Events</h4>
704
+ <div class="value">${metrics.total_events}<span class="unit">events</span></div>
705
+ </div>
706
+ <div class="metric-card">
707
+ <h4>Duration</h4>
708
+ <div class="value">${metrics.total_duration_ms.toFixed(2)}<span class="unit">ms</span></div>
709
+ </div>
710
+ <div class="metric-card">
711
+ <h4>Model Calls</h4>
712
+ <div class="value">${metrics.model_calls}<span class="unit">calls</span></div>
713
+ </div>
714
+ <div class="metric-card">
715
+ <h4>Tool Calls</h4>
716
+ <div class="value">${metrics.tool_calls}<span class="unit">calls</span></div>
717
+ </div>
718
+ <div class="metric-card">
719
+ <h4>Errors</h4>
720
+ <div class="value" style="color: ${metrics.errors > 0 ? '#ef4444' : '#10b981'}">${metrics.errors}<span class="unit">errors</span></div>
721
+ </div>
722
+ <div class="metric-card">
723
+ <h4>Error Rate</h4>
724
+ <div class="value">${(metrics.error_rate * 100).toFixed(1)}<span class="unit">%</span></div>
725
+ </div>
726
+ ${metrics.total_tokens > 0 ? `
727
+ <div class="metric-card">
728
+ <h4>Total Tokens</h4>
729
+ <div class="value">${metrics.total_tokens}<span class="unit">tokens</span></div>
730
+ </div>
731
+ <div class="metric-card">
732
+ <h4>Prompt Tokens</h4>
733
+ <div class="value">${metrics.prompt_tokens}<span class="unit">tokens</span></div>
734
+ </div>
735
+ <div class="metric-card">
736
+ <h4>Completion Tokens</h4>
737
+ <div class="value">${metrics.completion_tokens}<span class="unit">tokens</span></div>
738
+ </div>
739
+ ` : ''}
740
+ ${metrics.avg_model_latency_ms > 0 ? `
741
+ <div class="metric-card">
742
+ <h4>Avg Model Latency</h4>
743
+ <div class="value">${metrics.avg_model_latency_ms.toFixed(2)}<span class="unit">ms</span></div>
744
+ </div>
745
+ ` : ''}
746
+ ${metrics.avg_tool_latency_ms > 0 ? `
747
+ <div class="metric-card">
748
+ <h4>Avg Tool Latency</h4>
749
+ <div class="value">${metrics.avg_tool_latency_ms.toFixed(2)}<span class="unit">ms</span></div>
750
+ </div>
751
+ ` : ''}
752
+ </div>
753
+ `;
754
+
755
+ // Add charts
756
+ if (metrics.total_tokens > 0) {
757
+ html += `
758
+ <div class="chart-container">
759
+ <h3>Token Usage</h3>
760
+ <canvas id="tokenChart"></canvas>
761
+ </div>
762
+ `;
763
+ }
764
+
765
+ if (timeline.length > 0) {
766
+ html += `
767
+ <div class="chart-container">
768
+ <h3>Event Timeline</h3>
769
+ <canvas id="timelineChart"></canvas>
770
+ </div>
771
+ `;
772
+ }
773
+
774
+ // Add timeline events
775
+ html += `
776
+ <div class="timeline-container">
777
+ <h3>Event Timeline</h3>
778
+ <div id="timeline-events"></div>
779
+ </div>
780
+ `;
781
+
782
+ // Add steps
783
+ if (steps.length > 0) {
784
+ html += `
785
+ <div class="steps-container">
786
+ <h3>Execution Steps</h3>
787
+ ${steps.map(step => `
788
+ <div class="step-card">
789
+ <div class="step-header">
790
+ <span class="step-id">${step.step_id}</span>
791
+ <span class="step-duration">${step.duration_ms.toFixed(2)}ms</span>
792
+ </div>
793
+ <div style="font-size: 12px; color: #6b7280; margin-bottom: 10px;">
794
+ Type: ${step.step_type} | Events: ${step.events.length}
795
+ </div>
796
+ ${step.error ? `
797
+ <div class="step-error">Error: ${step.error}</div>
798
+ ` : ''}
799
+ </div>
800
+ `).join('')}
801
+ </div>
802
+ `;
803
+ }
804
+
805
+ document.getElementById('detail-content').innerHTML = html;
806
+
807
+ // Render charts
808
+ if (metrics.total_tokens > 0) {
809
+ renderTokenChart(metrics);
810
+ }
811
+
812
+ if (timeline.length > 0) {
813
+ renderTimelineChart(timeline);
814
+ renderTimelineEvents(timeline);
815
+ }
816
+ }
817
+
818
+ function renderTokenChart(metrics) {
819
+ const ctx = document.getElementById('tokenChart');
820
+ if (charts.tokenChart) {
821
+ charts.tokenChart.destroy();
822
+ }
823
+
824
+ charts.tokenChart = new Chart(ctx, {
825
+ type: 'doughnut',
826
+ data: {
827
+ labels: ['Prompt Tokens', 'Completion Tokens'],
828
+ datasets: [{
829
+ data: [metrics.prompt_tokens, metrics.completion_tokens],
830
+ backgroundColor: ['#3b82f6', '#10b981'],
831
+ }]
832
+ },
833
+ options: {
834
+ responsive: true,
835
+ plugins: {
836
+ legend: {
837
+ position: 'bottom',
838
+ }
839
+ }
840
+ }
841
+ });
842
+ }
843
+
844
+ function renderTimelineChart(timeline) {
845
+ const ctx = document.getElementById('timelineChart');
846
+ if (charts.timelineChart) {
847
+ charts.timelineChart.destroy();
848
+ }
849
+
850
+ const eventTypes = {};
851
+ timeline.forEach(entry => {
852
+ const event = entry.event || {};
853
+ const type = event.type || 'unknown';
854
+ eventTypes[type] = (eventTypes[type] || 0) + 1;
855
+ });
856
+
857
+ charts.timelineChart = new Chart(ctx, {
858
+ type: 'bar',
859
+ data: {
860
+ labels: Object.keys(eventTypes),
861
+ datasets: [{
862
+ label: 'Event Count',
863
+ data: Object.values(eventTypes),
864
+ backgroundColor: '#3b82f6',
865
+ }]
866
+ },
867
+ options: {
868
+ responsive: true,
869
+ plugins: {
870
+ legend: {
871
+ display: false,
872
+ }
873
+ },
874
+ scales: {
875
+ y: {
876
+ beginAtZero: true,
877
+ }
878
+ }
879
+ }
880
+ });
881
+ }
882
+
883
+ function renderTimelineEvents(timeline) {
884
+ const container = document.getElementById('timeline-events');
885
+ container.innerHTML = timeline.map(entry => {
886
+ const event = entry.event || {};
887
+ const eventType = event.type || 'unknown';
888
+ const typeClass = eventType.includes('error') ? 'error' :
889
+ eventType.includes('model') ? 'model' :
890
+ eventType.includes('tool') ? 'tool' : '';
891
+
892
+ const eventData = event.data || {};
893
+ const details = JSON.stringify(eventData).substring(0, 100);
894
+ const relativeTime = entry.relative_time_ms || 0;
895
+
896
+ return `
897
+ <div class="timeline-event ${typeClass}" onclick="showEventDetails(${entry.index || 0})">
898
+ <div class="event-time">${relativeTime.toFixed(2)}ms</div>
899
+ <div class="event-type">${eventType}</div>
900
+ <div class="event-details">${details}${JSON.stringify(eventData).length > 100 ? '...' : ''}</div>
901
+ </div>
902
+ `;
903
+ }).join('');
904
+ }
905
+
906
+ function showEventDetails(index) {
907
+ // Could show modal with full event details
908
+ alert('Event details at index ' + index);
909
+ }
910
+
911
+ function closeDetail() {
912
+ document.getElementById('trace-detail').classList.remove('active');
913
+ currentTraceId = null;
914
+
915
+ // Destroy charts
916
+ Object.values(charts).forEach(chart => chart.destroy());
917
+ charts = {};
918
+ }
919
+
920
+ // Load traces on page load
921
+ loadTraces();
922
+
923
+ // Auto-refresh every 5 seconds
924
+ setInterval(loadTraces, 5000);
925
+ </script>
926
+ </body>
927
+ </html>
928
+ """
929
+
930
+
931
+ def serve_command(
932
+ port: int = typer.Option(8080, "--port", "-p", help="Port to run server on"),
933
+ trace_dir: str = typer.Option(
934
+ ".routekit/traces", "--trace-dir", "-t", help="Directory containing trace files"
935
+ ),
936
+ host: str = typer.Option("127.0.0.1", "--host", "-h", help="Host to bind to"),
937
+ ) -> None:
938
+ """Start the trace visualization web server.
939
+
940
+ Examples:
941
+ routkitai serve
942
+ routkitai serve --port 3000
943
+ routkitai serve --host 0.0.0.0 --port 8080
944
+ """
945
+ import uvicorn
946
+
947
+ # Set trace directory as environment variable for API endpoints
948
+ os.environ["ROUTEKIT_TRACE_DIR"] = trace_dir
949
+
950
+ try:
951
+ from rich.console import Console
952
+
953
+ console = Console()
954
+ console.print("\n[bold green]šŸš€ Starting routkitai Trace Viewer[/bold green]")
955
+ console.print(f"[dim]Server running at http://{host}:{port}[/dim]")
956
+ console.print(f"[dim]Trace directory: {trace_dir}[/dim]\n")
957
+ except ImportError:
958
+ print("\nšŸš€ Starting routkitai Trace Viewer")
959
+ print(f"Server running at http://{host}:{port}")
960
+ print(f"Trace directory: {trace_dir}\n")
961
+
962
+ uvicorn.run(app, host=host, port=port, log_level="info")
963
+
964
+
965
+ if __name__ == "__main__" and app is not None:
966
+ serve_command()