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.
tracellm/utils.py ADDED
@@ -0,0 +1,390 @@
1
+ import csv
2
+ import time
3
+ import uuid
4
+ from datetime import datetime, timezone
5
+ from pathlib import Path
6
+ from typing import Any
7
+
8
+ from rich.align import Align
9
+ from rich.console import Console, Group
10
+ from rich.live import Live
11
+ from rich.panel import Panel
12
+ from rich.progress import BarColumn, Progress, SpinnerColumn, TextColumn, TimeElapsedColumn
13
+ from rich.rule import Rule
14
+ from rich.table import Table
15
+ from rich.text import Text
16
+ from rich.tree import Tree
17
+
18
+ console = Console()
19
+ SLOW_TRACE_THRESHOLD_MS = 1500.0
20
+ WARNING_TRACE_THRESHOLD_MS = 900.0
21
+
22
+
23
+ def estimate_tokens(*parts: Any) -> int:
24
+ text = " ".join(str(part) for part in parts if part is not None)
25
+ if not text.strip():
26
+ return 0
27
+ return max(1, len(text.split()) + len(text) // 4)
28
+
29
+
30
+ def build_tool_step(
31
+ tool_name: str,
32
+ input_data: dict[str, Any],
33
+ output_data: dict[str, Any],
34
+ duration: float,
35
+ success: bool = True,
36
+ ) -> dict[str, Any]:
37
+ return {
38
+ "step_id": str(uuid.uuid4()),
39
+ "tool_name": tool_name,
40
+ "input": input_data,
41
+ "output": output_data,
42
+ "duration": round(duration, 2),
43
+ "success": success,
44
+ "timestamp": datetime.now(timezone.utc).isoformat(),
45
+ }
46
+
47
+
48
+ def simulate_step(
49
+ steps: list[dict[str, Any]],
50
+ tool_name: str,
51
+ input_data: dict[str, Any],
52
+ output_data: dict[str, Any],
53
+ min_delay: float,
54
+ max_delay: float,
55
+ random_module: Any,
56
+ success: bool = True,
57
+ ) -> dict[str, Any]:
58
+ started = time.perf_counter()
59
+ time.sleep(random_module.uniform(min_delay, max_delay))
60
+ duration = round((time.perf_counter() - started) * 1000, 2)
61
+ step = build_tool_step(
62
+ tool_name=tool_name,
63
+ input_data=input_data,
64
+ output_data=output_data,
65
+ duration=duration,
66
+ success=success,
67
+ )
68
+ steps.append(step)
69
+ return step
70
+
71
+
72
+ def coerce_response(result: Any) -> str:
73
+ if isinstance(result, dict):
74
+ response = result.get("response")
75
+ if response is not None:
76
+ return str(response)
77
+ if result is None:
78
+ return ""
79
+ return str(result)
80
+
81
+
82
+ def coerce_steps(result: Any) -> list[dict[str, Any]]:
83
+ if isinstance(result, dict) and isinstance(result.get("steps"), list):
84
+ return [step for step in result["steps"] if isinstance(step, dict)]
85
+ return []
86
+
87
+
88
+ def coerce_retry_count(result: Any) -> int:
89
+ if isinstance(result, dict):
90
+ value = result.get("retry_count", 0)
91
+ try:
92
+ return max(0, int(value))
93
+ except (TypeError, ValueError):
94
+ return 0
95
+ return 0
96
+
97
+
98
+ def coerce_status(result: Any, retry_count: int) -> str:
99
+ if isinstance(result, dict):
100
+ status = str(result.get("status") or "").lower()
101
+ if status in {"success", "warning", "failed"}:
102
+ return status
103
+ if retry_count > 0:
104
+ return "warning"
105
+ return "success"
106
+
107
+
108
+ def coerce_failure_reason(result: Any) -> str | None:
109
+ if isinstance(result, dict):
110
+ failure_reason = result.get("failure_reason")
111
+ if failure_reason:
112
+ return str(failure_reason)
113
+ return None
114
+
115
+
116
+ def status_style(status: str) -> str:
117
+ normalized = status.lower()
118
+ if normalized == "success":
119
+ return "bold green"
120
+ if normalized == "warning":
121
+ return "bold yellow"
122
+ return "bold red"
123
+
124
+
125
+ def environment_style(environment: str) -> str:
126
+ normalized = environment.lower()
127
+ if normalized == "production":
128
+ return "bold red"
129
+ if normalized == "staging":
130
+ return "bold yellow"
131
+ return "bold cyan"
132
+
133
+
134
+ def latency_style(latency_ms: float) -> str:
135
+ if latency_ms >= SLOW_TRACE_THRESHOLD_MS:
136
+ return "bold red"
137
+ if latency_ms >= WARNING_TRACE_THRESHOLD_MS:
138
+ return "bold yellow"
139
+ return "green"
140
+
141
+
142
+ def ensure_export_dir() -> Path:
143
+ export_dir = Path.cwd() / "exports"
144
+ export_dir.mkdir(parents=True, exist_ok=True)
145
+ return export_dir
146
+
147
+
148
+ def export_timestamp() -> str:
149
+ return datetime.now(timezone.utc).strftime("%Y%m%d-%H%M%S")
150
+
151
+
152
+ def write_csv(path: Path, rows: list[dict[str, Any]], fieldnames: list[str]) -> None:
153
+ with path.open("w", newline="", encoding="utf-8") as csv_file:
154
+ writer = csv.DictWriter(csv_file, fieldnames=fieldnames)
155
+ writer.writeheader()
156
+ writer.writerows(rows)
157
+
158
+
159
+ def render_status_badge(status: str) -> Text:
160
+ normalized = status.lower()
161
+ if normalized == "success":
162
+ return Text(" SUCCESS ", style="bold white on #1a6b3c")
163
+ if normalized == "warning":
164
+ return Text(" WARNING ", style="bold #1a1a1a on #b8860b")
165
+ return Text(" FAILED ", style="bold white on #8b1a1a")
166
+
167
+
168
+ def build_trace_summary_table(trace_data: dict[str, Any]) -> Table:
169
+ table = Table.grid(padding=(0, 3), collapse_padding=True)
170
+ table.add_column(style="bright_black", no_wrap=True)
171
+ table.add_column(style="white")
172
+ table.add_row("Trace ID", str(trace_data["trace_id"]))
173
+ table.add_row("Prompt", str(trace_data["prompt"])[:80])
174
+ table.add_row("Model", str(trace_data["model_name"]))
175
+ table.add_row("Project", str(trace_data.get("project_name") or trace_data.get("project_id") or "default"))
176
+ table.add_row(
177
+ "Environment",
178
+ f'[{environment_style(str(trace_data.get("environment") or "development"))}]{str(trace_data.get("environment") or "development")}[/]',
179
+ )
180
+ latency = float(trace_data["latency"])
181
+ table.add_row("Latency", f'[{latency_style(latency)}]{latency:.2f} ms[/]')
182
+ table.add_row("Token Count", f"{trace_data['token_count']:,}")
183
+ table.add_row("Retries", str(trace_data["retry_count"]))
184
+ table.add_row("Steps", str(len(trace_data["steps"])))
185
+ status_badge = render_status_badge(str(trace_data["status"]))
186
+ table.add_row("Status", status_badge)
187
+ return table
188
+
189
+
190
+ def build_steps_table(steps: list[dict[str, Any]]) -> Table:
191
+ table = Table(title=" Steps", box=None, padding=(0, 2), header_style="dim")
192
+ table.add_column("#", style="bright_black", width=3)
193
+ table.add_column("Tool", style="white")
194
+ table.add_column("Duration", justify="right", style="bright_black")
195
+ table.add_column("Status", justify="center")
196
+ table.add_column("Detail", style="dim")
197
+ for index, step in enumerate(steps, start=1):
198
+ success = bool(step.get("success", True))
199
+ duration = float(step.get("duration", 0.0))
200
+ detail = ""
201
+ if not success:
202
+ detail = "retry"
203
+ elif duration >= SLOW_TRACE_THRESHOLD_MS:
204
+ detail = "spike"
205
+ table.add_row(
206
+ str(index),
207
+ str(step.get("tool_name", "unknown")),
208
+ f'[{latency_style(duration)}]{duration:.0f} ms[/]',
209
+ "[green]OK[/]" if success else "[red]RETRY[/]",
210
+ detail,
211
+ )
212
+ return table
213
+
214
+
215
+ def render_trace_panel(trace_data: dict[str, Any], title: str = "Trace") -> Panel:
216
+ return Panel(
217
+ build_trace_summary_table(trace_data),
218
+ title=f"[bold]{title}[/bold]",
219
+ subtitle=f"[{status_style(str(trace_data['status']))}]{str(trace_data['status']).upper()}[/]",
220
+ border_style="bright_black",
221
+ padding=(1, 2),
222
+ )
223
+
224
+
225
+ def render_trace_report(trace_data: dict[str, Any]) -> None:
226
+ console.print()
227
+ console.print(render_trace_panel(trace_data, "TraceLLM Trace"))
228
+ console.print()
229
+ console.print(build_steps_table(trace_data["steps"]))
230
+ console.print()
231
+ response_preview = str(trace_data.get("response") or "")
232
+ if response_preview:
233
+ console.print(
234
+ Panel.fit(
235
+ response_preview[:600] + ("..." if len(response_preview) > 600 else ""),
236
+ title="Response Preview",
237
+ border_style="bright_black",
238
+ padding=(1, 2),
239
+ )
240
+ )
241
+ console.print()
242
+
243
+
244
+ def render_replay_tree(
245
+ steps: list[dict[str, Any]],
246
+ active_index: int | None = None,
247
+ ) -> Tree:
248
+ tree = Tree("", hide_root=True)
249
+ for i, step in enumerate(steps, 1):
250
+ tool_name = step.get("tool_name", "unknown")
251
+ duration = float(step.get("duration", 0.0))
252
+ success = bool(step.get("success", True))
253
+ dur_str = f"[bright_black]{duration:.0f}ms[/bright_black]"
254
+ if active_index == i:
255
+ branch = tree.add(f"[cyan]▶[/cyan] [white]{tool_name}[/white] {dur_str}")
256
+ elif active_index is not None and i < active_index:
257
+ status = "[green]OK[/green]" if success else "[red]RETRY[/red]"
258
+ branch = tree.add(f"[green]✓[/green] [dim]{tool_name}[/dim] {dur_str} {status}")
259
+ elif active_index is not None and i > active_index:
260
+ branch = tree.add(f" [dim]{tool_name}[/dim] {dur_str}")
261
+ else:
262
+ status = "[green]OK[/green]" if success else "[red]RETRY[/red]"
263
+ branch = tree.add(f" [white]{tool_name}[/white] {dur_str} {status}")
264
+ return tree
265
+
266
+
267
+ def render_replay_report(trace_result: dict[str, Any]) -> None:
268
+ steps = trace_result.get("steps", [])
269
+ console.print()
270
+ console.print("Replay complete", style="bold white")
271
+ console.print()
272
+ tree = render_replay_tree(steps, active_index=len(steps))
273
+ console.print(Panel(tree, border_style="bright_black", padding=(1, 2)))
274
+ console.print()
275
+
276
+
277
+ def build_progress_bar(description: str, total: int) -> Progress:
278
+ progress = Progress(
279
+ SpinnerColumn(style="white"),
280
+ TextColumn("[bright_black]{task.description}"),
281
+ BarColumn(bar_width=24, style="bright_black", pulse_style="white"),
282
+ TextColumn("[bright_black]{task.completed}/{task.total}[/bright_black]"),
283
+ TimeElapsedColumn(),
284
+ expand=True,
285
+ )
286
+ progress.add_task(description, total=total)
287
+ return progress
288
+
289
+
290
+ def build_live_trace_screen(
291
+ prompt: str,
292
+ model_name: str,
293
+ finished_steps: list[dict[str, Any]],
294
+ current_label: str,
295
+ ) -> Panel:
296
+ progress = Progress(
297
+ SpinnerColumn(style="white"),
298
+ TextColumn("[white]{task.description}"),
299
+ BarColumn(bar_width=28, style="bright_black", complete_style="white"),
300
+ TextColumn("[bright_black]{task.completed}/{task.total}[/bright_black]"),
301
+ TimeElapsedColumn(),
302
+ expand=True,
303
+ )
304
+ total = max(1, len(finished_steps) + 1)
305
+ task_id = progress.add_task(current_label, total=total, completed=len(finished_steps))
306
+
307
+ timeline = Table.grid(padding=(0, 2))
308
+ timeline.add_column(style="bright_black", width=3)
309
+ timeline.add_column(style="white")
310
+ timeline.add_column(style="bright_black", justify="right")
311
+ timeline.add_column(style="bright_black")
312
+
313
+ for index, step in enumerate(finished_steps[-5:], start=max(1, len(finished_steps) - 4)):
314
+ duration = float(step.get("duration", 0.0))
315
+ signal = "retry" if not step.get("success", True) else "steady"
316
+ if duration >= SLOW_TRACE_THRESHOLD_MS:
317
+ signal = "spike"
318
+ timeline.add_row(
319
+ str(index),
320
+ str(step.get("tool_name", "unknown")),
321
+ f'[{latency_style(duration)}]{duration:.0f} ms[/]',
322
+ signal,
323
+ )
324
+
325
+ body = Group(
326
+ Align.left(Text(prompt, style="bold white")),
327
+ Text(f"model: {model_name}", style="bright_black"),
328
+ Rule(style="bright_black"),
329
+ progress,
330
+ Rule(style="bright_black"),
331
+ timeline,
332
+ )
333
+ progress.update(task_id, completed=len(finished_steps))
334
+ return Panel(body, title="Live Trace", border_style="bright_black", padding=(1, 2))
335
+
336
+
337
+ def trace_command_footer(trace_data: dict[str, Any]) -> None:
338
+ status = str(trace_data["status"])
339
+ badge = render_status_badge(status)
340
+ console.print(
341
+ Panel.fit(
342
+ f"[bright_black]trace_id[/bright_black] {trace_data['trace_id']}\n"
343
+ f"[bright_black]model[/bright_black] {trace_data.get('model_name', 'unknown')}\n"
344
+ f"[bright_black]latency[/bright_black] {float(trace_data['latency']):.2f} ms\n"
345
+ f"[bright_black]tokens[/bright_black] {trace_data['token_count']:,}\n"
346
+ f"[bright_black]retries[/bright_black] {trace_data['retry_count']}\n"
347
+ f"{badge}",
348
+ title="Trace Complete",
349
+ border_style="bright_black",
350
+ padding=(1, 2),
351
+ )
352
+ )
353
+
354
+
355
+ def render_project_credentials(
356
+ project_id: str,
357
+ name: str,
358
+ environment: str,
359
+ api_key: str,
360
+ description: str,
361
+ ) -> None:
362
+ console.print()
363
+ console.print(
364
+ Panel.fit(
365
+ f"[bold white]{name}[/bold white]\n\n"
366
+ f"[bright_black]project_id[/bright_black] {project_id}\n"
367
+ f"[bright_black]env[/bright_black] [{environment_style(environment)}]{environment}[/]\n"
368
+ f"[bright_black]api_key[/bright_black] [bold]{api_key}[/bold]\n"
369
+ f"[bright_black]desc[/bright_black] {description or 'n/a'}\n\n"
370
+ f"[dim]Save this key — it will not be shown again.[/dim]",
371
+ title="Project Credentials",
372
+ border_style="bright_black",
373
+ padding=(1, 2),
374
+ )
375
+ )
376
+ console.print()
377
+
378
+
379
+ def render_export_success(path: Path, count: int) -> None:
380
+ console.print()
381
+ console.print(
382
+ Panel.fit(
383
+ f"[bold white]{count} traces exported[/bold white]\n"
384
+ f"[dim]{path}[/dim]",
385
+ title="Export Complete",
386
+ border_style="bright_black",
387
+ padding=(1, 2),
388
+ )
389
+ )
390
+ console.print()
@@ -0,0 +1,30 @@
1
+ Metadata-Version: 2.4
2
+ Name: tracellm-cli
3
+ Version: 0.1.0
4
+ Summary: TraceLLM — Open-source LLM observability, tracing, and replay infrastructure.
5
+ Requires-Python: >=3.11
6
+ Description-Content-Type: text/markdown
7
+ Requires-Dist: fastapi
8
+ Requires-Dist: typer
9
+ Requires-Dist: rich
10
+ Requires-Dist: motor
11
+ Requires-Dist: pymongo
12
+ Requires-Dist: python-dotenv
13
+ Requires-Dist: openai
14
+ Requires-Dist: langchain-core
15
+ Requires-Dist: httpx
16
+
17
+ # TraceLLM Backend Package
18
+
19
+ Install in editable mode with:
20
+
21
+ ```bash
22
+ pip install -e .
23
+ ```
24
+
25
+ This package exposes:
26
+
27
+ - `from tracellm import trace`
28
+ - `tracellm demo`
29
+ - `tracellm trace`
30
+ - `tracellm replay`
@@ -0,0 +1,43 @@
1
+ app/__init__.py,sha256=JutOO7DgN1mmqlmpKf8ZlqnGhUdE40epGJqL112fado,44
2
+ app/main.py,sha256=e1gnhv_SGW1v2rsgdkZm7MuOQp576mKty_QU7TO1ZWQ,1279
3
+ app/database/__init__.py,sha256=29zIkjz9AWBcYKi9tX5hTNFjDVyZESXgAZITBQ8yQpQ,37
4
+ app/database/mongodb.py,sha256=o5vPtRRywUscrNbVfRH81JnIfK5BCr58v2iC3RidE6M,2535
5
+ app/database/project_service.py,sha256=TCMj8lpV2VUteHrqNvOhJV_nTqa_7X6VXGvDUrtKMes,3505
6
+ app/database/trace_service.py,sha256=m_l5pI-attakMkqhcFxhauTXk_nGLXq1rUV6UrDGcQU,15724
7
+ app/models/__init__.py,sha256=L-pNP1hY9c1-yHusD01UMoP3Wypwgk8RbwGl8dACB8U,361
8
+ app/models/health.py,sha256=evVu_-tvLLCaAj9REnd99uDIKXrmKhJymuZSOjik7dg,83
9
+ app/models/project.py,sha256=95UdSHGX9UWrWzeHqdcl36QUq7IFd9oHDQK0Lh67t3k,701
10
+ app/models/trace.py,sha256=GNJEi3TgF3J_yKpdga83XUCVS7EKYj5ahSBBgPyOzQU,2105
11
+ app/models/trace_model.py,sha256=s-9w0EmvFYmYvEVZsgZUoaF9KRX-IO_IncFFFMjZfNA,1455
12
+ app/routes/__init__.py,sha256=vmvaMIxzv70y1slvUI0gQRArGJsQ3rYL182dEbFGCU4,38
13
+ app/routes/health.py,sha256=H8yvu0QZXgBbIz32d6wQiWlxQJO8oip-zEK3R-Gcsx4,254
14
+ app/routes/observability.py,sha256=yQtHHO2AllKDdeQPLp56oADx8vzDcYlvpMibZ5XIMwM,2075
15
+ app/routes/projects.py,sha256=2nVuKzuhbQ1qnkQ_cMTTnyORBktrLxklfONLM-zpeSA,864
16
+ app/websocket/__init__.py,sha256=AGZnybL2CnfpHb-RXyoKWJ5fGLMG-KQQr-_uz76vqSs,38
17
+ app/websocket/socket.py,sha256=EwCeJgmMDQjm1BkLjpYTaFWp2Z8nbTsdDNmbR1Uaplg,1960
18
+ sdk/__init__.py,sha256=dLvZniY4ws9BEHsDn3-qaqUt_KwzLp9jQYFlJoPV-Rw,182
19
+ sdk/tracer.py,sha256=-X9KVGF6jX_e4BjuoIlXIR3SCxxA32mU06x37ZfP-BE,160
20
+ tracellm/__init__.py,sha256=TiQxkgG6R-117qY-6uMzaFfH2jPrLl57Mj4d03q8yIs,317
21
+ tracellm/banner.py,sha256=qh55DZ-pFuJHPU-ghoObP0iZieYpsiBY5N0JtufDCg4,984
22
+ tracellm/cli.py,sha256=Hg9MGdVB0oRVwPktKnKlEvOyBdcIKLrpFlpmMdNkKmM,4918
23
+ tracellm/db.py,sha256=Arm_4oeN2c4MM7cGh82pz_DJkyV8Z9jSao4aPFwnn_w,2459
24
+ tracellm/exporter.py,sha256=cJ0373hPy2i1ErmPhX01bNVLfcHrx9soa1wceOVpcQ0,2186
25
+ tracellm/mascot.py,sha256=vLagQV1teRmnKOVR01KGymQZaoS186sITVzazVwcmBU,1245
26
+ tracellm/monitor.py,sha256=qXWI5B2IQ68jMyU3oBIATT1ur-zw1VR5SCLXjQta3ms,14441
27
+ tracellm/palette.py,sha256=yyVliLzxaOvfM926VoiethpOFl1vs5NRMY_68Lwz8nY,6231
28
+ tracellm/replay.py,sha256=2nsyAM5OvHWGa6FsEdNloG_ZqshYCSKkXAsS8XJh1SQ,3579
29
+ tracellm/startup.py,sha256=1UbC6V7dnHr_g6cbUJNTYX4cKIs-dVDJjQq7oOmQTbE,3984
30
+ tracellm/summary.py,sha256=fuvN7IVbgzfYkP0-2nivhs_n2Qu44nMgKDBeBv0RtFw,1694
31
+ tracellm/trace_stream.py,sha256=GElD1ZX6y6rwZBKtTML0gRw9ZV4bowEvCEQVLmPJ4wY,2271
32
+ tracellm/tracer.py,sha256=WSEtYRI3OSMnBd28KQU1skhkfSkbJEJC7PCweiT1duE,23119
33
+ tracellm/tree_renderer.py,sha256=8rnJKXGjqv-doA58LHxojNCKj7ja_ZjgCzy5Z1ZNjvI,2416
34
+ tracellm/utils.py,sha256=4ZdRzXSmsARi7KdICAKRQaMVgRzxPSMGunfu3KHG6JM,13291
35
+ tracellm/integrations/__init__.py,sha256=0pYLI4TyWSgLuRXLXUg9XLVTYW4bKSKgMgbhxOL0Nec,203
36
+ tracellm/integrations/langchain.py,sha256=KeZ6dQTdED25sDS4tbOu5pXbvzo1BSWndIvqRU1ByzY,7359
37
+ tracellm/integrations/openai.py,sha256=-JT_TgHqEfG9gEv7x6LHCriCQD7J69xkX--xWIwNiAE,7759
38
+ tracellm/integrations/tool_tracer.py,sha256=yx05HzF1Mo_RdJTvGm0_n2SXZ991_JPF97HpA1uimLI,6066
39
+ tracellm_cli-0.1.0.dist-info/METADATA,sha256=_m8DWOt3Kk4ANAdHW4npZMqrWZC9mXYsXzD2s4vhk1s,651
40
+ tracellm_cli-0.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
41
+ tracellm_cli-0.1.0.dist-info/entry_points.txt,sha256=Z8FCRzoX43LJy-4T0k5PDzTYDyLe8WVhb9Lv97uOl7w,47
42
+ tracellm_cli-0.1.0.dist-info/top_level.txt,sha256=6y0UHevq1y28aKxTBGYIO9YncbN7EuvPQG8v84RvNic,17
43
+ tracellm_cli-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ tracellm = tracellm.cli:main
@@ -0,0 +1,3 @@
1
+ app
2
+ sdk
3
+ tracellm