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/models/project.py ADDED
@@ -0,0 +1,32 @@
1
+ from datetime import datetime, timezone
2
+ from typing import Literal
3
+
4
+ from pydantic import Field
5
+
6
+ from app.models.trace import MongoFriendlyModel
7
+
8
+
9
+ def _utcnow() -> datetime:
10
+ return datetime.now(timezone.utc)
11
+
12
+
13
+ EnvironmentLiteral = Literal["development", "staging", "production"]
14
+
15
+
16
+ class ProjectSchema(MongoFriendlyModel):
17
+ project_id: str
18
+ name: str
19
+ description: str = ""
20
+ created_at: datetime = Field(default_factory=_utcnow)
21
+
22
+
23
+ class ApiKeySchema(MongoFriendlyModel):
24
+ key: str
25
+ project_id: str
26
+ environment: str
27
+ created_at: datetime = Field(default_factory=_utcnow)
28
+
29
+
30
+ class ProjectCreateResponse(MongoFriendlyModel):
31
+ project: ProjectSchema
32
+ api_key: ApiKeySchema
app/models/trace.py ADDED
@@ -0,0 +1,71 @@
1
+ from datetime import datetime, timezone
2
+ from typing import Any, Literal, Optional
3
+
4
+ from pydantic import BaseModel, ConfigDict, Field, field_validator
5
+
6
+
7
+ def _utcnow() -> datetime:
8
+ return datetime.now(timezone.utc)
9
+
10
+
11
+ TraceStatus = Literal["success", "warning", "failed"]
12
+
13
+
14
+ class MongoFriendlyModel(BaseModel):
15
+ """Base settings shared by MongoDB-backed models."""
16
+
17
+ model_config = ConfigDict(
18
+ populate_by_name=True,
19
+ json_encoders={datetime: lambda value: value.isoformat()},
20
+ )
21
+
22
+
23
+ class StepSchema(MongoFriendlyModel):
24
+ step_id: str
25
+ tool_name: str
26
+ input: dict[str, Any] = Field(default_factory=dict)
27
+ output: dict[str, Any] = Field(default_factory=dict)
28
+ duration: float
29
+ success: bool
30
+ timestamp: datetime = Field(default_factory=_utcnow)
31
+
32
+ @field_validator("duration")
33
+ @classmethod
34
+ def validate_duration(cls, value: float) -> float:
35
+ if value < 0:
36
+ raise ValueError("duration must be greater than or equal to 0")
37
+ return value
38
+
39
+
40
+ class TraceSchema(MongoFriendlyModel):
41
+ trace_id: str
42
+ prompt: str
43
+ response: Optional[str] = None
44
+ latency: float
45
+ token_count: int
46
+ model_name: Optional[str] = None
47
+ project_id: str = "default"
48
+ project_name: Optional[str] = None
49
+ api_key: Optional[str] = None
50
+ environment: str = "development"
51
+ status: TraceStatus
52
+ steps: list[StepSchema] = Field(default_factory=list)
53
+ retry_count: int = 0
54
+ slow_request: bool = False
55
+ failure_reason: Optional[str] = None
56
+ created_at: datetime = Field(default_factory=_utcnow)
57
+ updated_at: datetime = Field(default_factory=_utcnow)
58
+
59
+ @field_validator("latency")
60
+ @classmethod
61
+ def validate_latency(cls, value: float) -> float:
62
+ if value < 0:
63
+ raise ValueError("latency must be greater than or equal to 0")
64
+ return value
65
+
66
+ @field_validator("token_count", "retry_count")
67
+ @classmethod
68
+ def validate_non_negative_ints(cls, value: int) -> int:
69
+ if value < 0:
70
+ raise ValueError("value must be greater than or equal to 0")
71
+ return value
@@ -0,0 +1,62 @@
1
+ from typing import Optional
2
+
3
+ from pydantic import BaseModel, Field
4
+
5
+ from app.models.trace import TraceSchema
6
+
7
+
8
+ class TraceListResponse(BaseModel):
9
+ total: int
10
+ items: list[TraceSchema]
11
+
12
+
13
+ class TraceFilters(BaseModel):
14
+ latency_min: Optional[float] = None
15
+ latency_max: Optional[float] = None
16
+ status: Optional[str] = None
17
+ model: Optional[str] = None
18
+ project_id: Optional[str] = None
19
+ environment: Optional[str] = None
20
+ token_min: Optional[int] = None
21
+ token_max: Optional[int] = None
22
+ limit: int = Field(default=50, ge=1, le=200)
23
+
24
+
25
+ class AnalyticsSummary(BaseModel):
26
+ total_traces: int
27
+ success_rate: float
28
+ average_latency: float
29
+ p95_latency: float
30
+ total_token_usage: int
31
+ failed_traces: int
32
+ warning_traces: int
33
+ retries: int
34
+ slow_requests: int
35
+
36
+
37
+ class AnalyticsChartPoint(BaseModel):
38
+ label: str
39
+ latency: float
40
+ tokens: int
41
+ traces: int
42
+
43
+
44
+ class AnalyticsBreakdownItem(BaseModel):
45
+ key: str
46
+ count: int
47
+
48
+
49
+ class AnalyticsResponse(BaseModel):
50
+ summary: AnalyticsSummary
51
+ charts: list[AnalyticsChartPoint]
52
+ status_breakdown: list[AnalyticsBreakdownItem]
53
+ model_breakdown: list[AnalyticsBreakdownItem]
54
+ project_breakdown: list[AnalyticsBreakdownItem]
55
+ recent_failures: list[TraceSchema]
56
+
57
+
58
+ class FailureResponse(BaseModel):
59
+ failed_traces: list[TraceSchema]
60
+ retries: list[TraceSchema]
61
+ slow_requests: list[TraceSchema]
62
+ totals: dict[str, int]
app/routes/__init__.py ADDED
@@ -0,0 +1 @@
1
+ """API route modules for TraceLLM."""
app/routes/health.py ADDED
@@ -0,0 +1,10 @@
1
+ from fastapi import APIRouter
2
+
3
+ from app.models.health import HealthResponse
4
+
5
+ router = APIRouter()
6
+
7
+
8
+ @router.get("/", response_model=HealthResponse)
9
+ async def health_check() -> HealthResponse:
10
+ return HealthResponse(message="TraceLLM backend running")
@@ -0,0 +1,60 @@
1
+ from fastapi import APIRouter, Query
2
+
3
+ from app.database.trace_service import get_analytics, get_failures, get_trace_by_id, list_traces
4
+ from app.models.trace import TraceSchema
5
+ from app.models.trace_model import AnalyticsResponse, FailureResponse, TraceFilters, TraceListResponse
6
+
7
+ router = APIRouter()
8
+
9
+
10
+ @router.get("/traces", response_model=TraceListResponse)
11
+ async def get_traces(
12
+ latency_min: float | None = Query(default=None, ge=0),
13
+ latency_max: float | None = Query(default=None, ge=0),
14
+ status: str | None = Query(default=None),
15
+ model: str | None = Query(default=None),
16
+ project_id: str | None = Query(default=None),
17
+ environment: str | None = Query(default=None),
18
+ token_min: int | None = Query(default=None, ge=0),
19
+ token_max: int | None = Query(default=None, ge=0),
20
+ limit: int = Query(default=50, ge=1, le=200),
21
+ ) -> TraceListResponse:
22
+ filters = TraceFilters(
23
+ latency_min=latency_min,
24
+ latency_max=latency_max,
25
+ status=status,
26
+ model=model,
27
+ project_id=project_id,
28
+ environment=environment,
29
+ token_min=token_min,
30
+ token_max=token_max,
31
+ limit=limit,
32
+ )
33
+ return await list_traces(filters)
34
+
35
+
36
+ @router.get("/traces/{trace_id}", response_model=TraceSchema)
37
+ async def get_trace(trace_id: str) -> TraceSchema:
38
+ return await get_trace_by_id(trace_id)
39
+
40
+
41
+ @router.get("/analytics", response_model=AnalyticsResponse)
42
+ async def analytics(
43
+ project_id: str | None = Query(default=None),
44
+ environment: str | None = Query(default=None),
45
+ ) -> AnalyticsResponse:
46
+ return await get_analytics(
47
+ TraceFilters(project_id=project_id, environment=environment)
48
+ )
49
+
50
+
51
+ @router.get("/failures", response_model=FailureResponse)
52
+ async def failures(
53
+ limit: int = Query(default=25, ge=1, le=100),
54
+ project_id: str | None = Query(default=None),
55
+ environment: str | None = Query(default=None),
56
+ ) -> FailureResponse:
57
+ return await get_failures(
58
+ limit=limit,
59
+ filters=TraceFilters(limit=limit, project_id=project_id, environment=environment),
60
+ )
app/routes/projects.py ADDED
@@ -0,0 +1,25 @@
1
+ from fastapi import APIRouter, Query
2
+
3
+ from app.database.project_service import create_project, list_api_keys, list_projects
4
+ from app.models.project import ApiKeySchema, ProjectCreateResponse, ProjectSchema
5
+
6
+ router = APIRouter()
7
+
8
+
9
+ @router.get("/projects", response_model=list[ProjectSchema])
10
+ async def get_projects() -> list[ProjectSchema]:
11
+ return await list_projects()
12
+
13
+
14
+ @router.get("/api-keys", response_model=list[ApiKeySchema])
15
+ async def get_api_keys(project_id: str | None = Query(default=None)) -> list[ApiKeySchema]:
16
+ return await list_api_keys(project_id=project_id)
17
+
18
+
19
+ @router.post("/projects", response_model=ProjectCreateResponse)
20
+ async def create_project_route(
21
+ name: str,
22
+ environment: str,
23
+ description: str = "",
24
+ ) -> ProjectCreateResponse:
25
+ return await create_project(name=name, description=description, environment=environment)
@@ -0,0 +1 @@
1
+ """WebSocket modules for TraceLLM."""
@@ -0,0 +1,64 @@
1
+ import asyncio
2
+ import logging
3
+ from typing import Any
4
+
5
+ from fastapi import APIRouter, WebSocket, WebSocketDisconnect
6
+
7
+ router = APIRouter()
8
+ logger = logging.getLogger(__name__)
9
+
10
+
11
+ class ConnectionManager:
12
+ """Small websocket manager for broadcasting observability events."""
13
+
14
+ def __init__(self) -> None:
15
+ self._connections: list[WebSocket] = []
16
+ self._lock = asyncio.Lock()
17
+
18
+ async def connect(self, websocket: WebSocket) -> None:
19
+ await websocket.accept()
20
+ async with self._lock:
21
+ self._connections.append(websocket)
22
+
23
+ async def disconnect(self, websocket: WebSocket) -> None:
24
+ async with self._lock:
25
+ if websocket in self._connections:
26
+ self._connections.remove(websocket)
27
+
28
+ async def broadcast(self, payload: dict[str, Any]) -> None:
29
+ async with self._lock:
30
+ connections = list(self._connections)
31
+
32
+ stale_connections: list[WebSocket] = []
33
+ for connection in connections:
34
+ try:
35
+ await connection.send_json(payload)
36
+ except Exception:
37
+ logger.exception("Failed to send websocket payload")
38
+ stale_connections.append(connection)
39
+
40
+ for stale_connection in stale_connections:
41
+ await self.disconnect(stale_connection)
42
+
43
+
44
+ manager = ConnectionManager()
45
+
46
+
47
+ @router.websocket("/ws")
48
+ async def websocket_endpoint(websocket: WebSocket) -> None:
49
+ await manager.connect(websocket)
50
+ await websocket.send_json(
51
+ {
52
+ "type": "system.connected",
53
+ "status": "connected",
54
+ "message": "TraceLLM websocket active",
55
+ }
56
+ )
57
+
58
+ try:
59
+ while True:
60
+ message = await websocket.receive_json()
61
+ if message.get("type") == "ping":
62
+ await websocket.send_json({"type": "system.pong", "timestamp": message.get("ts")})
63
+ except WebSocketDisconnect:
64
+ await manager.disconnect(websocket)
sdk/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ from tracellm import trace, wrap_openai, TraceOpenAI, TracellmCallbackHandler, trace_tool
2
+
3
+ __all__ = ["trace", "wrap_openai", "TraceOpenAI", "TracellmCallbackHandler", "trace_tool"]
sdk/tracer.py ADDED
@@ -0,0 +1,8 @@
1
+ from tracellm.cli import app
2
+ from tracellm.tracer import llm_response, trace
3
+
4
+ __all__ = ["app", "llm_response", "trace"]
5
+
6
+
7
+ if __name__ == "__main__":
8
+ app()
tracellm/__init__.py ADDED
@@ -0,0 +1,6 @@
1
+ from tracellm.tracer import trace
2
+ from tracellm.integrations.openai import wrap_openai, TraceOpenAI
3
+ from tracellm.integrations.langchain import TracellmCallbackHandler
4
+ from tracellm.integrations.tool_tracer import trace_tool
5
+
6
+ __all__ = ["trace", "wrap_openai", "TraceOpenAI", "TracellmCallbackHandler", "trace_tool"]
tracellm/banner.py ADDED
@@ -0,0 +1,34 @@
1
+ """Premium startup banner for TraceLLM CLI."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from datetime import datetime
6
+
7
+ from rich.box import DOUBLE
8
+ from rich.panel import Panel
9
+ from rich.table import Table
10
+ from rich.text import Text
11
+ from rich.align import Align
12
+
13
+
14
+ def render_banner(
15
+ version: str = "0.2.0",
16
+ environment: str = "development",
17
+ ) -> Panel:
18
+ """Render a centered premium startup banner panel."""
19
+ now = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
20
+
21
+ inner = Table.grid(padding=(0, 0))
22
+ inner.add_column(justify="center")
23
+ inner.add_row(Text(f"TraceLLM v{version}", style="bold white"))
24
+ inner.add_row(Text("Open-source LLM Observability", style="bright_black"))
25
+ inner.add_row(Text(""))
26
+ inner.add_row(Text(f"Started at {now}", style="dim"))
27
+ inner.add_row(Text(f"Environment: {environment}", style="dim"))
28
+
29
+ return Panel(
30
+ Align.center(inner),
31
+ box=DOUBLE,
32
+ border_style="bright_black",
33
+ padding=(1, 4),
34
+ )
tracellm/cli.py ADDED
@@ -0,0 +1,124 @@
1
+ import os
2
+
3
+ import typer
4
+
5
+ from tracellm.db import create_project_with_key
6
+ from tracellm.exporter import export_traces
7
+ from tracellm.replay import replay_trace
8
+ from tracellm.startup import run_start
9
+ from tracellm.tracer import llm_response, run_live_trace
10
+ from tracellm.utils import console, render_project_credentials
11
+
12
+ app = typer.Typer(help="TraceLLM SDK and developer CLI.")
13
+
14
+
15
+ @app.callback(invoke_without_command=True)
16
+ def cli_entry(ctx: typer.Context) -> None:
17
+ if ctx.invoked_subcommand is None:
18
+ from tracellm.palette import run_palette
19
+ run_palette(app)
20
+
21
+
22
+ @app.command()
23
+ def demo() -> None:
24
+ """Generate a realistic demo trace."""
25
+ result = llm_response()
26
+ retries = result.get("retry_count", 0)
27
+ steps = len(result.get("steps", []))
28
+ console.print()
29
+ console.print("[bold white]Demo complete[/bold white]")
30
+ console.print(f"[bright_black]{steps} steps, {retries} retries[/bright_black]")
31
+ console.print()
32
+
33
+
34
+ @app.command()
35
+ def start(
36
+ port: int = typer.Option(8000, "--port", "-p", help="Port for the API server."),
37
+ dashboard: bool = typer.Option(False, "--dashboard", "-d", help="Open the dashboard in your browser."),
38
+ dashboard_port: int = typer.Option(3000, "--dashboard-port", help="Port for the frontend dashboard."),
39
+ ) -> None:
40
+ """Start the TraceLLM observability stack (backend + dashboard)."""
41
+ run_start(port=port, dashboard_port=dashboard_port, launch_dashboard=dashboard)
42
+
43
+
44
+ @app.command("trace")
45
+ def trace_command(
46
+ prompt: str = typer.Argument(..., help="Prompt to trace."),
47
+ model: str = typer.Option("gpt-4.1-mini", "--model", help="Model name label for the trace."),
48
+ project: str = typer.Option("default", "--project", help="Project identifier or display name."),
49
+ environment: str = typer.Option("development", "--environment", help="Environment label."),
50
+ api_key: str | None = typer.Option(None, "--api-key", help="TraceLLM API key."),
51
+ ) -> None:
52
+ """Run a production-style traced prompt simulation."""
53
+ run_live_trace(
54
+ prompt=prompt,
55
+ model_name=model,
56
+ project=project,
57
+ environment=environment,
58
+ api_key=api_key,
59
+ render=True,
60
+ )
61
+
62
+
63
+ @app.command()
64
+ def replay(
65
+ trace_id: str = typer.Argument(..., help="Trace ID to replay from MongoDB."),
66
+ speed: float = typer.Option(1.0, "--speed", min=0.1, help="Replay speed multiplier."),
67
+ show_response: bool = typer.Option(False, "--show-response", help="Show the full saved response after replay."),
68
+ ) -> None:
69
+ """Replay a saved trace from MongoDB in the terminal."""
70
+ replay_trace(trace_id=trace_id, speed=speed, show_response=show_response)
71
+
72
+
73
+ @app.command()
74
+ def monitor(
75
+ refresh: float = typer.Option(2.0, "--refresh", "-r", help="Polling interval in seconds."),
76
+ limit: int = typer.Option(10, "--limit", "-l", help="Number of recent traces to show."),
77
+ ws_host: str = typer.Option(os.environ.get("TRACELLM_WS_HOST", "127.0.0.1"), "--ws-host", help="Backend WebSocket host. Falls back to TRACELLM_WS_HOST env."),
78
+ ws_port: int = typer.Option(int(os.environ.get("TRACELLM_WS_PORT", "8000")), "--ws-port", help="Backend WebSocket port. Falls back to TRACELLM_WS_PORT env."),
79
+ ) -> None:
80
+ """Watch incoming real traces in realtime."""
81
+ from tracellm.monitor import run_monitor as _run_monitor
82
+ try:
83
+ _run_monitor(refresh=refresh, limit=limit, ws_host=ws_host, ws_port=ws_port)
84
+ except KeyboardInterrupt:
85
+ console.print("\n[yellow]Monitor stopped.[/yellow]")
86
+
87
+
88
+ @app.command()
89
+ def export(
90
+ format: str = typer.Option("json", "--format", help="Export format: json or csv."),
91
+ limit: int = typer.Option(100, "--limit", min=1, max=1000, help="How many recent traces to export."),
92
+ ) -> None:
93
+ """Export traces from MongoDB."""
94
+ normalized = format.lower()
95
+ if normalized not in {"json", "csv"}:
96
+ raise typer.BadParameter("format must be one of: json, csv")
97
+ export_traces(export_format=normalized, limit=limit)
98
+
99
+
100
+ @app.command("create-project")
101
+ def create_project_command() -> None:
102
+ """Create a project and generate a secure API key."""
103
+ name = typer.prompt("Project name").strip()
104
+ description = typer.prompt("Description", default="", show_default=False).strip()
105
+ environment = typer.prompt("Environment", default="development").strip().lower()
106
+ if environment not in {"development", "staging", "production"}:
107
+ raise typer.BadParameter("environment must be development, staging, or production")
108
+
109
+ response = create_project_with_key(name=name, description=description, environment=environment)
110
+ render_project_credentials(
111
+ project_id=response.project.project_id,
112
+ name=response.project.name,
113
+ environment=response.api_key.environment,
114
+ api_key=response.api_key.key,
115
+ description=response.project.description,
116
+ )
117
+
118
+
119
+ def main() -> None:
120
+ app()
121
+
122
+
123
+ if __name__ == "__main__":
124
+ main()
tracellm/db.py ADDED
@@ -0,0 +1,75 @@
1
+ """Database helpers with persistent async lifecycle management."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import logging
7
+ from typing import Any, TypeVar
8
+
9
+ from app.database.project_service import create_project, get_project_by_api_key, list_projects
10
+ from app.database.trace_service import get_trace_by_id, list_traces, save_trace, save_trace_sync
11
+ from app.models.project import ApiKeySchema, ProjectCreateResponse, ProjectSchema
12
+ from app.models.trace import TraceSchema
13
+ from app.models.trace_model import TraceFilters
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+ T = TypeVar("T")
18
+
19
+ _ASYNC_LOOP: asyncio.AbstractEventLoop | None = None
20
+
21
+
22
+ def _run_async(coro: Any) -> T:
23
+ """Run a coroutine synchronously using a persistent event loop.
24
+
25
+ Unlike ``asyncio.run()`` this does **not** close the loop after the
26
+ coroutine completes, which avoids “event loop is closed” errors when
27
+ Motor / MongoDB background tasks reference the loop.
28
+ """
29
+ global _ASYNC_LOOP
30
+
31
+ # If an event loop is already running (e.g. inside FastAPI / uvicorn),
32
+ # schedule on that loop instead.
33
+ try:
34
+ running = asyncio.get_running_loop()
35
+ if running.is_running():
36
+ future = asyncio.run_coroutine_threadsafe(coro, running)
37
+ return future.result()
38
+ except RuntimeError:
39
+ pass
40
+
41
+ # Reuse or create a persistent loop for synchronous callers (CLI).
42
+ if _ASYNC_LOOP is None or _ASYNC_LOOP.is_closed():
43
+ _ASYNC_LOOP = asyncio.new_event_loop()
44
+ asyncio.set_event_loop(_ASYNC_LOOP)
45
+
46
+ return _ASYNC_LOOP.run_until_complete(coro)
47
+
48
+
49
+ def save_trace_payload(trace_data: dict[str, Any]) -> None:
50
+ save_trace_sync(trace_data)
51
+
52
+
53
+ async def save_trace_payload_async(trace_data: dict[str, Any]) -> dict[str, Any]:
54
+ return await save_trace(trace_data)
55
+
56
+
57
+ def fetch_trace(trace_id: str) -> TraceSchema:
58
+ return _run_async(get_trace_by_id(trace_id))
59
+
60
+
61
+ def fetch_recent_traces(limit: int = 100) -> list[TraceSchema]:
62
+ response = _run_async(list_traces(TraceFilters(limit=limit)))
63
+ return response.items
64
+
65
+
66
+ def create_project_with_key(name: str, description: str, environment: str) -> ProjectCreateResponse:
67
+ return _run_async(create_project(name=name, description=description, environment=environment))
68
+
69
+
70
+ def fetch_projects() -> list[ProjectSchema]:
71
+ return _run_async(list_projects())
72
+
73
+
74
+ def resolve_api_key(api_key: str) -> ApiKeySchema:
75
+ return _run_async(get_project_by_api_key(api_key))
tracellm/exporter.py ADDED
@@ -0,0 +1,65 @@
1
+ import json
2
+ from pathlib import Path
3
+
4
+ from tracellm.db import fetch_recent_traces
5
+ from tracellm.utils import console, ensure_export_dir, export_timestamp, render_export_success, write_csv
6
+
7
+
8
+ def export_traces(export_format: str, limit: int = 100) -> Path:
9
+ console.print()
10
+ console.print("[bold white]Exporting traces...[/bold white]")
11
+
12
+ traces = fetch_recent_traces(limit=limit)
13
+ export_dir = ensure_export_dir()
14
+ timestamp = export_timestamp()
15
+
16
+ if export_format == "json":
17
+ path = export_dir / f"traces-{timestamp}.json"
18
+ payload = [trace.model_dump(mode="json") for trace in traces]
19
+ path.write_text(json.dumps(payload, indent=2), encoding="utf-8")
20
+ else:
21
+ path = export_dir / f"traces-{timestamp}.csv"
22
+ rows = [
23
+ {
24
+ "trace_id": trace.trace_id,
25
+ "project_id": trace.project_id,
26
+ "project_name": trace.project_name,
27
+ "environment": trace.environment,
28
+ "prompt": trace.prompt,
29
+ "model_name": trace.model_name,
30
+ "status": trace.status,
31
+ "latency": trace.latency,
32
+ "token_count": trace.token_count,
33
+ "retry_count": trace.retry_count,
34
+ "slow_request": trace.slow_request,
35
+ "failure_reason": trace.failure_reason,
36
+ "created_at": trace.created_at.isoformat(),
37
+ "updated_at": trace.updated_at.isoformat(),
38
+ "step_count": len(trace.steps),
39
+ }
40
+ for trace in traces
41
+ ]
42
+ write_csv(
43
+ path,
44
+ rows,
45
+ [
46
+ "trace_id",
47
+ "project_id",
48
+ "project_name",
49
+ "environment",
50
+ "prompt",
51
+ "model_name",
52
+ "status",
53
+ "latency",
54
+ "token_count",
55
+ "retry_count",
56
+ "slow_request",
57
+ "failure_reason",
58
+ "created_at",
59
+ "updated_at",
60
+ "step_count",
61
+ ],
62
+ )
63
+
64
+ render_export_success(path, len(traces))
65
+ return path
@@ -0,0 +1,4 @@
1
+ from tracellm.integrations.openai import wrap_openai, TraceOpenAI
2
+ from tracellm.integrations.langchain import TracellmCallbackHandler
3
+
4
+ __all__ = ["wrap_openai", "TraceOpenAI", "TracellmCallbackHandler"]