tracellm-cli 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.
- app/__init__.py +1 -0
- app/database/__init__.py +1 -0
- app/database/mongodb.py +94 -0
- app/database/project_service.py +97 -0
- app/database/trace_service.py +417 -0
- app/main.py +44 -0
- app/models/__init__.py +14 -0
- app/models/health.py +5 -0
- app/models/project.py +32 -0
- app/models/trace.py +71 -0
- app/models/trace_model.py +62 -0
- app/routes/__init__.py +1 -0
- app/routes/health.py +10 -0
- app/routes/observability.py +60 -0
- app/routes/projects.py +25 -0
- app/websocket/__init__.py +1 -0
- app/websocket/socket.py +64 -0
- sdk/__init__.py +3 -0
- sdk/tracer.py +8 -0
- tracellm/__init__.py +6 -0
- tracellm/banner.py +34 -0
- tracellm/cli.py +124 -0
- tracellm/db.py +75 -0
- tracellm/exporter.py +65 -0
- tracellm/integrations/__init__.py +4 -0
- tracellm/integrations/langchain.py +186 -0
- tracellm/integrations/openai.py +234 -0
- tracellm/integrations/tool_tracer.py +151 -0
- tracellm/mascot.py +49 -0
- tracellm/monitor.py +381 -0
- tracellm/palette.py +186 -0
- tracellm/replay.py +80 -0
- tracellm/startup.py +121 -0
- tracellm/summary.py +53 -0
- tracellm/trace_stream.py +68 -0
- tracellm/tracer.py +598 -0
- tracellm/tree_renderer.py +78 -0
- tracellm/utils.py +390 -0
- tracellm_cli-0.1.0.dist-info/METADATA +30 -0
- tracellm_cli-0.1.0.dist-info/RECORD +43 -0
- tracellm_cli-0.1.0.dist-info/WHEEL +5 -0
- tracellm_cli-0.1.0.dist-info/entry_points.txt +2 -0
- tracellm_cli-0.1.0.dist-info/top_level.txt +3 -0
tracellm/startup.py
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import subprocess
|
|
3
|
+
import sys
|
|
4
|
+
import time
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
import httpx
|
|
8
|
+
from dotenv import load_dotenv
|
|
9
|
+
from rich.panel import Panel
|
|
10
|
+
from rich.progress import BarColumn, Progress, SpinnerColumn, TextColumn, TimeElapsedColumn
|
|
11
|
+
from rich.table import Table
|
|
12
|
+
|
|
13
|
+
from tracellm.banner import render_banner
|
|
14
|
+
from tracellm.mascot import MascotState, message
|
|
15
|
+
from tracellm.utils import console
|
|
16
|
+
|
|
17
|
+
load_dotenv()
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _check_mongodb() -> bool:
|
|
21
|
+
try:
|
|
22
|
+
import pymongo
|
|
23
|
+
mongo_url = os.getenv("MONGO_URL")
|
|
24
|
+
if not mongo_url:
|
|
25
|
+
console.print("[yellow]MONGO_URL not set — traces won't be persisted[/yellow]")
|
|
26
|
+
return False
|
|
27
|
+
client = pymongo.MongoClient(mongo_url, serverSelectionTimeoutMS=3000)
|
|
28
|
+
client.admin.command("ping")
|
|
29
|
+
client.close()
|
|
30
|
+
return True
|
|
31
|
+
except Exception:
|
|
32
|
+
console.print("[yellow]MongoDB not reachable — traces won't be persisted[/yellow]")
|
|
33
|
+
return False
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _start_fastapi(port: int) -> subprocess.Popen:
|
|
37
|
+
backend_dir = Path(__file__).resolve().parent.parent
|
|
38
|
+
env = os.environ.copy()
|
|
39
|
+
env["PYTHONPATH"] = str(backend_dir)
|
|
40
|
+
|
|
41
|
+
process = subprocess.Popen(
|
|
42
|
+
[sys.executable, "-m", "uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", str(port), "--reload"],
|
|
43
|
+
cwd=backend_dir,
|
|
44
|
+
env=env,
|
|
45
|
+
stdout=subprocess.PIPE,
|
|
46
|
+
stderr=subprocess.STDOUT,
|
|
47
|
+
)
|
|
48
|
+
return process
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _health_check(port: int, timeout: int = 15) -> bool:
|
|
52
|
+
start = time.time()
|
|
53
|
+
with console.status("[bold white]Waiting for API server...[/bold white]"):
|
|
54
|
+
while time.time() - start < timeout:
|
|
55
|
+
try:
|
|
56
|
+
response = httpx.get(f"http://127.0.0.1:{port}/", timeout=3)
|
|
57
|
+
if response.status_code == 200:
|
|
58
|
+
return True
|
|
59
|
+
except (httpx.ConnectError, httpx.TimeoutException):
|
|
60
|
+
pass
|
|
61
|
+
time.sleep(0.5)
|
|
62
|
+
return False
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _render_status(mongodb_ok: bool, api_ok: bool, port: int, dashboard_port: int, launch_dashboard: bool) -> None:
|
|
66
|
+
table = Table.grid(padding=(0, 2))
|
|
67
|
+
table.add_column()
|
|
68
|
+
table.add_column()
|
|
69
|
+
|
|
70
|
+
table.add_row(
|
|
71
|
+
"API Server",
|
|
72
|
+
f"[green]●[/green] http://127.0.0.1:{port}" if api_ok else "[red]●[/red] Failed",
|
|
73
|
+
)
|
|
74
|
+
table.add_row(
|
|
75
|
+
"MongoDB",
|
|
76
|
+
"[green]● Connected[/green]" if mongodb_ok else "[yellow]● Skipped[/yellow]",
|
|
77
|
+
)
|
|
78
|
+
table.add_row(
|
|
79
|
+
"Dashboard",
|
|
80
|
+
f"[green]● http://localhost:{dashboard_port}[/green]" if launch_dashboard else "[dim]● Not launched[/dim]",
|
|
81
|
+
)
|
|
82
|
+
table.add_row(
|
|
83
|
+
"WebSocket",
|
|
84
|
+
f"[dim]ws://127.0.0.1:{port}/ws[/dim]",
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
console.print()
|
|
88
|
+
console.print(
|
|
89
|
+
Panel.fit(table, title="TraceLLM Stack", border_style="bright_black", padding=(1, 3))
|
|
90
|
+
)
|
|
91
|
+
console.print()
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def run_start(port: int = 8000, dashboard_port: int = 3000, launch_dashboard: bool = False) -> None:
|
|
95
|
+
console.print()
|
|
96
|
+
console.print(render_banner())
|
|
97
|
+
console.print()
|
|
98
|
+
console.print(message("TraceLLM starting...", MascotState.LOADING))
|
|
99
|
+
console.print()
|
|
100
|
+
|
|
101
|
+
mongodb_ok = _check_mongodb()
|
|
102
|
+
console.print(f" [green]✓[/green] MongoDB connected" if mongodb_ok else f" [red]✗[/red] MongoDB unavailable")
|
|
103
|
+
|
|
104
|
+
api_process = _start_fastapi(port)
|
|
105
|
+
api_ok = _health_check(port)
|
|
106
|
+
console.print(f" [green]✓[/green] API ready" if api_ok else f" [red]✗[/red] API failed")
|
|
107
|
+
console.print(f" [green]✓[/green] WebSocket ready" if api_ok else f" [red]✗[/red] WebSocket unavailable")
|
|
108
|
+
|
|
109
|
+
if launch_dashboard and api_ok:
|
|
110
|
+
import webbrowser
|
|
111
|
+
webbrowser.open(f"http://localhost:{dashboard_port}")
|
|
112
|
+
|
|
113
|
+
_render_status(mongodb_ok, api_ok, port, dashboard_port, launch_dashboard)
|
|
114
|
+
|
|
115
|
+
try:
|
|
116
|
+
api_process.wait()
|
|
117
|
+
except KeyboardInterrupt:
|
|
118
|
+
console.print("\n[yellow]Shutting down...[/yellow]")
|
|
119
|
+
api_process.terminate()
|
|
120
|
+
api_process.wait()
|
|
121
|
+
console.print("[green]Stopped.[/green]")
|
tracellm/summary.py
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"""Premium trace summary card."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from rich.panel import Panel
|
|
8
|
+
from rich.table import Table
|
|
9
|
+
from rich.text import Text
|
|
10
|
+
|
|
11
|
+
from tracellm.utils import render_status_badge
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def render_summary(trace_data: dict[str, Any]) -> Panel:
|
|
15
|
+
"""Render a professional trace summary card."""
|
|
16
|
+
status = str(trace_data["status"])
|
|
17
|
+
badge = render_status_badge(status)
|
|
18
|
+
border = "green" if status == "success" else "yellow" if status == "warning" else "red"
|
|
19
|
+
|
|
20
|
+
table = Table.grid(padding=(0, 3))
|
|
21
|
+
table.add_column(style="bright_black", width=14)
|
|
22
|
+
table.add_column(style="white")
|
|
23
|
+
|
|
24
|
+
table.add_row("Model", str(trace_data.get("model_name", "unknown")))
|
|
25
|
+
table.add_row("Latency", f"{float(trace_data['latency']):.2f} ms")
|
|
26
|
+
table.add_row("Tokens", f"{trace_data['token_count']:,}")
|
|
27
|
+
table.add_row("Retries", str(trace_data["retry_count"]))
|
|
28
|
+
table.add_row("Status", badge)
|
|
29
|
+
created = str(trace_data.get("created_at", ""))
|
|
30
|
+
if created:
|
|
31
|
+
try:
|
|
32
|
+
from datetime import datetime
|
|
33
|
+
dt = datetime.fromisoformat(created)
|
|
34
|
+
created = dt.strftime("%Y-%m-%d %H:%M:%S")
|
|
35
|
+
except (ValueError, TypeError):
|
|
36
|
+
pass
|
|
37
|
+
table.add_row("Timestamp", created)
|
|
38
|
+
table.add_row("Trace ID", str(trace_data["trace_id"]))
|
|
39
|
+
|
|
40
|
+
return Panel(
|
|
41
|
+
table,
|
|
42
|
+
title="\U0001f996 Trace Complete",
|
|
43
|
+
border_style=border,
|
|
44
|
+
padding=(1, 3),
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def print_summary(trace_data: dict[str, Any]) -> None:
|
|
49
|
+
"""Print the trace summary card to console."""
|
|
50
|
+
from tracellm.utils import console
|
|
51
|
+
console.print()
|
|
52
|
+
console.print(render_summary(trace_data))
|
|
53
|
+
console.print()
|
tracellm/trace_stream.py
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"""Live event stream for trace execution."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from rich.live import Live
|
|
9
|
+
from rich.panel import Panel
|
|
10
|
+
from rich.table import Table
|
|
11
|
+
from rich.text import Text
|
|
12
|
+
|
|
13
|
+
from tracellm.utils import console
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class TraceStream:
|
|
17
|
+
"""Realtime event stream that renders timestamped trace events."""
|
|
18
|
+
|
|
19
|
+
def __init__(self, prompt: str, model_name: str) -> None:
|
|
20
|
+
self.prompt = prompt
|
|
21
|
+
self.model_name = model_name
|
|
22
|
+
self.events: list[tuple[str, str, str]] = []
|
|
23
|
+
self._start = datetime.now()
|
|
24
|
+
self._live = Live(console=console, refresh_per_second=10, transient=True)
|
|
25
|
+
|
|
26
|
+
def __enter__(self) -> TraceStream:
|
|
27
|
+
self.emit("trace.start")
|
|
28
|
+
self._live.__enter__()
|
|
29
|
+
return self
|
|
30
|
+
|
|
31
|
+
def __exit__(self, *args: Any) -> None:
|
|
32
|
+
self.emit("trace.complete")
|
|
33
|
+
self._live.__exit__(*args)
|
|
34
|
+
|
|
35
|
+
def emit(self, event: str, detail: str = "") -> None:
|
|
36
|
+
"""Emit a trace event and update the live display."""
|
|
37
|
+
ts = datetime.now().strftime("%H:%M:%S")
|
|
38
|
+
self.events.append((ts, event, detail))
|
|
39
|
+
self._live.update(self._render())
|
|
40
|
+
|
|
41
|
+
def _render(self) -> Panel:
|
|
42
|
+
now = datetime.now()
|
|
43
|
+
elapsed = (now - self._start).total_seconds()
|
|
44
|
+
|
|
45
|
+
event_table = Table.grid(padding=(0, 2))
|
|
46
|
+
event_table.add_column(style="bright_black", width=10)
|
|
47
|
+
event_table.add_column(style="cyan")
|
|
48
|
+
event_table.add_column(style="dim", width=30)
|
|
49
|
+
|
|
50
|
+
for ts, evt, det in self.events:
|
|
51
|
+
event_table.add_row(f"[{ts}]", evt, det)
|
|
52
|
+
|
|
53
|
+
summary = Table.grid(padding=(0, 2))
|
|
54
|
+
summary.add_column(style="bright_black")
|
|
55
|
+
summary.add_column(style="white")
|
|
56
|
+
summary.add_row("Prompt", self.prompt[:60])
|
|
57
|
+
summary.add_row("Model", self.model_name)
|
|
58
|
+
summary.add_row("Elapsed", f"{elapsed:.1f}s")
|
|
59
|
+
summary.add_row("Events", str(len(self.events)))
|
|
60
|
+
|
|
61
|
+
body = Table.grid(padding=(0, 1))
|
|
62
|
+
body.add_column()
|
|
63
|
+
body.add_row(Text("Event Stream", style="bold white"))
|
|
64
|
+
body.add_row(event_table)
|
|
65
|
+
body.add_row(Text(""))
|
|
66
|
+
body.add_row(summary)
|
|
67
|
+
|
|
68
|
+
return Panel(body, title="Live Trace", border_style="bright_black", padding=(1, 2))
|