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.
- routekitai/__init__.py +53 -0
- routekitai/cli/__init__.py +18 -0
- routekitai/cli/main.py +40 -0
- routekitai/cli/replay.py +80 -0
- routekitai/cli/run.py +95 -0
- routekitai/cli/serve.py +966 -0
- routekitai/cli/test_agent.py +178 -0
- routekitai/cli/trace.py +209 -0
- routekitai/cli/trace_analyze.py +120 -0
- routekitai/cli/trace_search.py +126 -0
- routekitai/core/__init__.py +58 -0
- routekitai/core/agent.py +325 -0
- routekitai/core/errors.py +49 -0
- routekitai/core/hooks.py +174 -0
- routekitai/core/memory.py +54 -0
- routekitai/core/message.py +132 -0
- routekitai/core/model.py +91 -0
- routekitai/core/policies.py +373 -0
- routekitai/core/policy.py +85 -0
- routekitai/core/policy_adapter.py +133 -0
- routekitai/core/runtime.py +1403 -0
- routekitai/core/tool.py +148 -0
- routekitai/core/tools.py +180 -0
- routekitai/evals/__init__.py +13 -0
- routekitai/evals/dataset.py +75 -0
- routekitai/evals/metrics.py +101 -0
- routekitai/evals/runner.py +184 -0
- routekitai/graphs/__init__.py +12 -0
- routekitai/graphs/executors.py +457 -0
- routekitai/graphs/graph.py +164 -0
- routekitai/memory/__init__.py +13 -0
- routekitai/memory/episodic.py +242 -0
- routekitai/memory/kv.py +34 -0
- routekitai/memory/retrieval.py +192 -0
- routekitai/memory/vector.py +700 -0
- routekitai/memory/working.py +66 -0
- routekitai/message.py +29 -0
- routekitai/model.py +48 -0
- routekitai/observability/__init__.py +21 -0
- routekitai/observability/analyzer.py +314 -0
- routekitai/observability/exporters/__init__.py +10 -0
- routekitai/observability/exporters/base.py +30 -0
- routekitai/observability/exporters/jsonl.py +81 -0
- routekitai/observability/exporters/otel.py +119 -0
- routekitai/observability/spans.py +111 -0
- routekitai/observability/streaming.py +117 -0
- routekitai/observability/trace.py +144 -0
- routekitai/providers/__init__.py +9 -0
- routekitai/providers/anthropic.py +227 -0
- routekitai/providers/azure_openai.py +243 -0
- routekitai/providers/local.py +196 -0
- routekitai/providers/openai.py +321 -0
- routekitai/py.typed +0 -0
- routekitai/sandbox/__init__.py +12 -0
- routekitai/sandbox/filesystem.py +131 -0
- routekitai/sandbox/network.py +142 -0
- routekitai/sandbox/permissions.py +70 -0
- routekitai/tool.py +33 -0
- routekitai-0.1.0.dist-info/METADATA +328 -0
- routekitai-0.1.0.dist-info/RECORD +64 -0
- routekitai-0.1.0.dist-info/WHEEL +5 -0
- routekitai-0.1.0.dist-info/entry_points.txt +2 -0
- routekitai-0.1.0.dist-info/licenses/LICENSE +21 -0
- routekitai-0.1.0.dist-info/top_level.txt +1 -0
routekitai/cli/serve.py
ADDED
|
@@ -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()
|