magic-lab-observer-cli 0.1.1__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.
cli/__init__.py ADDED
@@ -0,0 +1 @@
1
+ """LLM Observatory CLI - Command-line interface for LLM Observatory."""
cli/db.py ADDED
@@ -0,0 +1,188 @@
1
+ """Database helper for local SQLite access."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import sqlite3
6
+ from pathlib import Path
7
+ from typing import Any
8
+
9
+ DEFAULT_DB_PATH = Path("observatory.db")
10
+
11
+ # DDL to create tables if the database is fresh/empty
12
+ _SCHEMA_SQL = """
13
+ CREATE TABLE IF NOT EXISTS traces (
14
+ id TEXT PRIMARY KEY,
15
+ name TEXT NOT NULL,
16
+ session_id TEXT,
17
+ start_time TIMESTAMP NOT NULL,
18
+ end_time TIMESTAMP,
19
+ status TEXT NOT NULL DEFAULT 'unset',
20
+ metadata JSON,
21
+ created_at TIMESTAMP
22
+ );
23
+ CREATE INDEX IF NOT EXISTS ix_traces_session_id ON traces (session_id);
24
+
25
+ CREATE TABLE IF NOT EXISTS spans (
26
+ id TEXT PRIMARY KEY,
27
+ trace_id TEXT NOT NULL REFERENCES traces(id) ON DELETE CASCADE,
28
+ parent_span_id TEXT REFERENCES spans(id),
29
+ name TEXT NOT NULL,
30
+ span_type TEXT NOT NULL DEFAULT 'generic',
31
+ start_time TIMESTAMP NOT NULL,
32
+ end_time TIMESTAMP,
33
+ status TEXT NOT NULL DEFAULT 'unset',
34
+ input JSON,
35
+ output JSON,
36
+ tokens_input INTEGER,
37
+ tokens_output INTEGER,
38
+ cost_usd NUMERIC(10, 6),
39
+ metadata JSON,
40
+ attributes JSON
41
+ );
42
+ CREATE INDEX IF NOT EXISTS ix_spans_trace_id ON spans (trace_id);
43
+ CREATE INDEX IF NOT EXISTS ix_spans_parent_span_id ON spans (parent_span_id);
44
+
45
+ CREATE TABLE IF NOT EXISTS evaluations (
46
+ id TEXT PRIMARY KEY,
47
+ trace_id TEXT NOT NULL REFERENCES traces(id) ON DELETE CASCADE,
48
+ span_id TEXT REFERENCES spans(id) ON DELETE CASCADE,
49
+ evaluator_type TEXT NOT NULL,
50
+ score NUMERIC(5, 4),
51
+ criteria JSON,
52
+ result JSON,
53
+ created_at TIMESTAMP
54
+ );
55
+ CREATE INDEX IF NOT EXISTS ix_evaluations_trace_id ON evaluations (trace_id);
56
+ CREATE INDEX IF NOT EXISTS ix_evaluations_span_id ON evaluations (span_id);
57
+ """
58
+
59
+
60
+ def get_connection(db_path: Path | None = None) -> sqlite3.Connection:
61
+ """Get a SQLite connection and ensure schema exists."""
62
+ path = db_path or DEFAULT_DB_PATH
63
+ conn = sqlite3.connect(str(path))
64
+ conn.row_factory = sqlite3.Row
65
+ conn.executescript(_SCHEMA_SQL)
66
+ return conn
67
+
68
+
69
+ def list_traces(
70
+ db_path: Path | None = None,
71
+ limit: int = 100,
72
+ offset: int = 0,
73
+ status: str | None = None,
74
+ name_search: str | None = None,
75
+ ) -> list[dict[str, Any]]:
76
+ """List traces from local SQLite database."""
77
+ conn = get_connection(db_path)
78
+ try:
79
+ query = "SELECT * FROM traces WHERE 1=1"
80
+ params: list[Any] = []
81
+
82
+ if status:
83
+ query += " AND status = ?"
84
+ params.append(status)
85
+
86
+ if name_search:
87
+ query += " AND name LIKE ?"
88
+ params.append(f"%{name_search}%")
89
+
90
+ query += " ORDER BY created_at DESC LIMIT ? OFFSET ?"
91
+ params.extend([limit, offset])
92
+
93
+ cursor = conn.execute(query, params)
94
+ return [dict(row) for row in cursor.fetchall()]
95
+ finally:
96
+ conn.close()
97
+
98
+
99
+ def get_trace(trace_id: str, db_path: Path | None = None) -> dict[str, Any] | None:
100
+ """Get a single trace by ID."""
101
+ conn = get_connection(db_path)
102
+ try:
103
+ cursor = conn.execute("SELECT * FROM traces WHERE id = ?", (trace_id,))
104
+ row = cursor.fetchone()
105
+ return dict(row) if row else None
106
+ finally:
107
+ conn.close()
108
+
109
+
110
+ def get_trace_spans(trace_id: str, db_path: Path | None = None) -> list[dict[str, Any]]:
111
+ """Get all spans for a trace."""
112
+ conn = get_connection(db_path)
113
+ try:
114
+ cursor = conn.execute(
115
+ "SELECT * FROM spans WHERE trace_id = ? ORDER BY start_time",
116
+ (trace_id,),
117
+ )
118
+ return [dict(row) for row in cursor.fetchall()]
119
+ finally:
120
+ conn.close()
121
+
122
+
123
+ def list_evaluations(
124
+ db_path: Path | None = None,
125
+ trace_id: str | None = None,
126
+ limit: int = 100,
127
+ offset: int = 0,
128
+ ) -> list[dict[str, Any]]:
129
+ """List evaluations from local SQLite database."""
130
+ conn = get_connection(db_path)
131
+ try:
132
+ query = "SELECT * FROM evaluations WHERE 1=1"
133
+ params: list[Any] = []
134
+
135
+ if trace_id:
136
+ query += " AND trace_id = ?"
137
+ params.append(trace_id)
138
+
139
+ query += " ORDER BY created_at DESC LIMIT ? OFFSET ?"
140
+ params.extend([limit, offset])
141
+
142
+ cursor = conn.execute(query, params)
143
+ return [dict(row) for row in cursor.fetchall()]
144
+ finally:
145
+ conn.close()
146
+
147
+
148
+ def get_stats(db_path: Path | None = None) -> dict[str, Any]:
149
+ """Get summary statistics from the database."""
150
+ conn = get_connection(db_path)
151
+ try:
152
+ stats: dict[str, Any] = {}
153
+
154
+ # Trace counts
155
+ cursor = conn.execute("SELECT COUNT(*) FROM traces")
156
+ stats["total_traces"] = cursor.fetchone()[0]
157
+
158
+ cursor = conn.execute("SELECT COUNT(*) FROM traces WHERE status = 'ok'")
159
+ stats["ok_traces"] = cursor.fetchone()[0]
160
+
161
+ cursor = conn.execute("SELECT COUNT(*) FROM traces WHERE status = 'error'")
162
+ stats["error_traces"] = cursor.fetchone()[0]
163
+
164
+ # Span counts
165
+ cursor = conn.execute("SELECT COUNT(*) FROM spans")
166
+ stats["total_spans"] = cursor.fetchone()[0]
167
+
168
+ # Token totals
169
+ cursor = conn.execute(
170
+ "SELECT SUM(tokens_input), SUM(tokens_output) FROM spans WHERE tokens_input IS NOT NULL"
171
+ )
172
+ row = cursor.fetchone()
173
+ stats["total_input_tokens"] = row[0] or 0
174
+ stats["total_output_tokens"] = row[1] or 0
175
+
176
+ # Cost total
177
+ cursor = conn.execute(
178
+ "SELECT SUM(cost_usd) FROM spans WHERE cost_usd IS NOT NULL"
179
+ )
180
+ stats["total_cost_usd"] = cursor.fetchone()[0] or 0.0
181
+
182
+ # Evaluation counts
183
+ cursor = conn.execute("SELECT COUNT(*) FROM evaluations")
184
+ stats["total_evaluations"] = cursor.fetchone()[0]
185
+
186
+ return stats
187
+ finally:
188
+ conn.close()
cli/main.py ADDED
@@ -0,0 +1,414 @@
1
+ """Main CLI application for LLM Observatory."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ from pathlib import Path
7
+
8
+ import click
9
+ from rich.console import Console
10
+ from rich.table import Table
11
+
12
+ console = Console()
13
+
14
+
15
+ @click.group()
16
+ @click.version_option(version="0.1.1", prog_name="llm-observatory")
17
+ @click.option("--api-key", envvar="OBSERVATORY_API_KEY", default=None, help="API key for backend authentication")
18
+ @click.pass_context
19
+ def cli(ctx: click.Context, api_key: str | None) -> None:
20
+ """LLM Observatory CLI - Open-source LLM observability."""
21
+ ctx.ensure_object(dict)
22
+ ctx.obj["api_key"] = api_key
23
+
24
+
25
+ # ── Local Database Commands ──────────────────────────────────────────
26
+
27
+
28
+ @cli.command()
29
+ @click.option("--db", type=click.Path(exists=True), default="observatory.db", help="SQLite database path")
30
+ @click.option("--limit", default=20, help="Number of traces to show")
31
+ @click.option("--status", type=click.Choice(["ok", "error", "unset"]), help="Filter by status")
32
+ @click.option("--search", help="Search trace names")
33
+ def traces(db: str, limit: int, status: str | None, search: str | None) -> None:
34
+ """List traces from local SQLite database."""
35
+ from cli.db import list_traces
36
+
37
+ rows = list_traces(Path(db), limit=limit, status=status, name_search=search)
38
+
39
+ if not rows:
40
+ console.print("[yellow]No traces found[/yellow]")
41
+ return
42
+
43
+ table = Table(title=f"Traces ({len(rows)} shown)")
44
+ table.add_column("ID", style="cyan", no_wrap=True)
45
+ table.add_column("Name", style="green")
46
+ table.add_column("Status", style="yellow")
47
+ table.add_column("Spans", justify="right")
48
+ table.add_column("Created", style="blue")
49
+
50
+ for row in rows:
51
+ table.add_row(
52
+ str(row["id"])[:8],
53
+ str(row["name"]),
54
+ str(row["status"]),
55
+ "",
56
+ str(row.get("created_at", "")),
57
+ )
58
+
59
+ console.print(table)
60
+
61
+
62
+ @cli.command()
63
+ @click.argument("trace_id")
64
+ @click.option("--db", type=click.Path(exists=True), default="observatory.db", help="SQLite database path")
65
+ def inspect(trace_id: str, db: str) -> None:
66
+ """Inspect a trace and its spans."""
67
+ from cli.db import get_trace, get_trace_spans
68
+
69
+ trace_data = get_trace(trace_id, Path(db))
70
+ if not trace_data:
71
+ console.print(f"[red]Trace {trace_id} not found[/red]")
72
+ return
73
+
74
+ # Print trace info
75
+ console.print(f"\n[bold cyan]Trace: {trace_data['name']}[/bold cyan]")
76
+ console.print(f" ID: {trace_data['id']}")
77
+ console.print(f" Status: {trace_data['status']}")
78
+ console.print(f" Created: {trace_data.get('created_at', 'N/A')}")
79
+
80
+ # Print spans
81
+ spans = get_trace_spans(trace_id, Path(db))
82
+ if spans:
83
+ console.print(f"\n[bold]Spans ({len(spans)}):[/bold]")
84
+ table = Table()
85
+ table.add_column("ID", style="cyan", no_wrap=True)
86
+ table.add_column("Name", style="green")
87
+ table.add_column("Type", style="yellow")
88
+ table.add_column("Status")
89
+ table.add_column("Tokens In", justify="right")
90
+ table.add_column("Tokens Out", justify="right")
91
+ table.add_column("Cost", justify="right")
92
+
93
+ for span in spans:
94
+ table.add_row(
95
+ str(span["id"])[:8],
96
+ str(span["name"]),
97
+ str(span["span_type"]),
98
+ str(span["status"]),
99
+ str(span.get("tokens_input") or ""),
100
+ str(span.get("tokens_output") or ""),
101
+ f"${span['cost_usd']:.6f}" if span.get("cost_usd") else "",
102
+ )
103
+
104
+ console.print(table)
105
+ else:
106
+ console.print("[yellow]No spans found[/yellow]")
107
+
108
+
109
+ @cli.command(name="import")
110
+ @click.argument("file", type=click.Path(exists=True))
111
+ @click.option("--db", type=click.Path(), default="observatory.db", help="SQLite database path")
112
+ @click.option("--dry-run", is_flag=True, help="Preview without writing")
113
+ def import_data(file: str, db: str, dry_run: bool) -> None:
114
+ """Import traces from a JSON export file."""
115
+ import json
116
+
117
+ from cli.db import get_connection
118
+
119
+ raw = json.loads(Path(file).read_text())
120
+ traces = raw.get("traces", [])
121
+
122
+ if not traces:
123
+ console.print("[yellow]No traces in file[/yellow]")
124
+ return
125
+
126
+ console.print(f"[blue]Importing {len(traces)} traces from {file}...[/blue]")
127
+
128
+ if dry_run:
129
+ for t in traces:
130
+ n = len(t.get("spans", []))
131
+ console.print(f" {t.get('name', '?')} — {n} spans")
132
+ console.print("[yellow]Dry run, no changes written[/yellow]")
133
+ return
134
+
135
+ conn = get_connection(Path(db))
136
+ try:
137
+ imported = 0
138
+ for trace in traces:
139
+ conn.execute(
140
+ "INSERT OR IGNORE INTO traces (id, name, session_id, status, metadata, start_time, end_time, created_at) VALUES (?,?,?,?,?,?,?,?)",
141
+ (
142
+ trace["id"],
143
+ trace["name"],
144
+ trace.get("session_id"),
145
+ trace.get("status", "unset"),
146
+ json.dumps(trace.get("metadata")),
147
+ trace.get("start_time"),
148
+ trace.get("end_time"),
149
+ trace.get("created_at"),
150
+ ),
151
+ )
152
+ for span in trace.get("spans", []):
153
+ conn.execute(
154
+ "INSERT OR IGNORE INTO spans (id, trace_id, parent_span_id, name, span_type, start_time, end_time, status, input, output, tokens_input, tokens_output, cost_usd, metadata, attributes) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)",
155
+ (
156
+ span["id"],
157
+ span.get("trace_id", trace["id"]),
158
+ span.get("parent_span_id"),
159
+ span["name"],
160
+ span.get("span_type", "generic"),
161
+ span.get("start_time"),
162
+ span.get("end_time"),
163
+ span.get("status", "unset"),
164
+ json.dumps(span.get("input")) if span.get("input") else None,
165
+ json.dumps(span.get("output")) if span.get("output") else None,
166
+ span.get("tokens_input"),
167
+ span.get("tokens_output"),
168
+ span.get("cost_usd"),
169
+ json.dumps(span.get("metadata")) if span.get("metadata") else None,
170
+ json.dumps(span.get("attributes")) if span.get("attributes") else None,
171
+ ),
172
+ )
173
+ imported += 1
174
+ conn.commit()
175
+ console.print(f"[green]Imported {imported} traces ({sum(len(t.get('spans', [])) for t in traces)} spans)[/green]")
176
+ finally:
177
+ conn.close()
178
+
179
+
180
+ @cli.command()
181
+ @click.option("--db", type=click.Path(exists=True), default="observatory.db", help="SQLite database path")
182
+ def stats(db: str) -> None:
183
+ """Show summary statistics."""
184
+ from cli.db import get_stats
185
+
186
+ data = get_stats(Path(db))
187
+
188
+ console.print("\n[bold cyan]LLM Observatory Statistics[/bold cyan]\n")
189
+ console.print(f" Total Traces: {data['total_traces']}")
190
+ console.print(f" OK: {data['ok_traces']}")
191
+ console.print(f" Error: {data['error_traces']}")
192
+ console.print(f" Total Spans: {data['total_spans']}")
193
+ console.print(f" Input Tokens: {data['total_input_tokens']:,}")
194
+ console.print(f" Output Tokens: {data['total_output_tokens']:,}")
195
+ console.print(f" Total Cost: ${data['total_cost_usd']:.6f}")
196
+ console.print(f" Evaluations: {data['total_evaluations']}")
197
+ console.print()
198
+
199
+
200
+ @cli.command()
201
+ @click.argument("trace_id")
202
+ @click.option("--db", type=click.Path(exists=True), default="observatory.db", help="SQLite database path")
203
+ @click.option("--format", "output_format", type=click.Choice(["json", "table"]), default="table")
204
+ def spans(trace_id: str, db: str, output_format: str) -> None:
205
+ """List spans for a trace."""
206
+ from cli.db import get_trace_spans
207
+
208
+ rows = get_trace_spans(trace_id, Path(db))
209
+
210
+ if not rows:
211
+ console.print("[yellow]No spans found[/yellow]")
212
+ return
213
+
214
+ if output_format == "json":
215
+ import json
216
+ console.print(json.dumps(rows, indent=2, default=str))
217
+ else:
218
+ table = Table(title=f"Spans for {trace_id[:8]}")
219
+ table.add_column("ID", style="cyan", no_wrap=True)
220
+ table.add_column("Name", style="green")
221
+ table.add_column("Type", style="yellow")
222
+ table.add_column("Status")
223
+ table.add_column("Duration (ms)", justify="right")
224
+ table.add_column("Tokens", justify="right")
225
+ table.add_column("Cost", justify="right")
226
+
227
+ for row in rows:
228
+ duration = ""
229
+ if row.get("start_time") and row.get("end_time"):
230
+ from datetime import datetime
231
+ try:
232
+ start = datetime.fromisoformat(str(row["start_time"]))
233
+ end = datetime.fromisoformat(str(row["end_time"]))
234
+ duration = f"{(end - start).total_seconds() * 1000:.0f}"
235
+ except (ValueError, TypeError):
236
+ pass
237
+
238
+ tokens = ""
239
+ if row.get("tokens_input") or row.get("tokens_output"):
240
+ tokens = f"{row.get('tokens_input', 0)}→{row.get('tokens_output', 0)}"
241
+
242
+ table.add_row(
243
+ str(row["id"])[:8],
244
+ str(row["name"]),
245
+ str(row["span_type"]),
246
+ str(row["status"]),
247
+ duration,
248
+ tokens,
249
+ f"${row['cost_usd']:.6f}" if row.get("cost_usd") else "",
250
+ )
251
+
252
+ console.print(table)
253
+
254
+
255
+ # ── Server Commands ──────────────────────────────────────────────────
256
+
257
+
258
+ @cli.command()
259
+ @click.option("--host", default="0.0.0.0", help="Host to bind to")
260
+ @click.option("--port", default=8000, help="Port to bind to")
261
+ @click.option("--reload", is_flag=True, help="Enable auto-reload")
262
+ @click.option("--db", default="sqlite+aiosqlite:///./observatory.db", help="Database URL")
263
+ def serve(host: str, port: int, reload: bool, db: str) -> None:
264
+ """Start the observatory backend server."""
265
+ try:
266
+ import uvicorn
267
+ except ImportError:
268
+ console.print("[red]Error: uvicorn is not installed.[/red]")
269
+ console.print("Install it with: pip install 'magic-lab-observer-backend[sqlite]'")
270
+ return
271
+
272
+ os.environ["DATABASE_URL"] = db
273
+
274
+ console.print(f"[green]Starting LLM Observatory server on {host}:{port}[/green]")
275
+ console.print(f"[green]Database: {db}[/green]")
276
+
277
+ try:
278
+ from app.main import app as fastapi_app
279
+ uvicorn.run(fastapi_app, host=host, port=port, reload=reload)
280
+ except ImportError:
281
+ console.print("[red]Error: backend app module not found.[/red]")
282
+ console.print("Make sure the backend is installed: pip install -e ../backend")
283
+ except (RuntimeError, OSError) as e:
284
+ console.print(f"[red]Server error: {e}[/red]")
285
+
286
+
287
+ @cli.command()
288
+ @click.option("--trace-id", required=True, help="Trace ID to evaluate")
289
+ @click.option(
290
+ "--evaluator",
291
+ type=click.Choice(["llm_judge", "rule_based", "human"]),
292
+ required=True,
293
+ help="Evaluator type",
294
+ )
295
+ @click.option("--criteria", help="Comma-separated criteria for evaluation")
296
+ @click.option("--endpoint", default="http://localhost:8000", help="API endpoint")
297
+ @click.pass_context
298
+ def evaluate(
299
+ ctx: click.Context,
300
+ trace_id: str,
301
+ evaluator: str,
302
+ criteria: str | None,
303
+ endpoint: str,
304
+ ) -> None:
305
+ """Run an evaluation on a trace."""
306
+ import httpx
307
+
308
+ console.print(f"[blue]Evaluating trace {trace_id} with {evaluator}...[/blue]")
309
+
310
+ criteria_list = criteria.split(",") if criteria else []
311
+
312
+ headers: dict[str, str] = {}
313
+ api_key = ctx.obj.get("api_key")
314
+ if api_key:
315
+ headers["Authorization"] = f"Bearer {api_key}"
316
+
317
+ try:
318
+ response = httpx.post(
319
+ f"{endpoint}/v1/evaluations/run",
320
+ json={
321
+ "trace_id": trace_id,
322
+ "evaluator_type": evaluator,
323
+ "criteria": [{"name": c.strip(), "description": c.strip()} for c in criteria_list] if criteria_list else [],
324
+ },
325
+ headers=headers,
326
+ )
327
+ response.raise_for_status()
328
+
329
+ result = response.json()
330
+ console.print("[green]Evaluation completed successfully![/green]")
331
+ console.print(f"Score: {result.get('score', 'N/A')}")
332
+ console.print(f"Passed: {result.get('passed', 'N/A')}")
333
+ if result.get("criteria"):
334
+ console.print("Criteria scores:")
335
+ for k, v in result["criteria"].items():
336
+ console.print(f" {k}: {v}")
337
+ except httpx.HTTPError as e:
338
+ console.print(f"[red]Error: {e}[/red]")
339
+
340
+
341
+ @cli.command()
342
+ @click.option("--db", type=click.Path(exists=True), default="observatory.db", help="SQLite database path")
343
+ @click.option("--format", "output_format", type=click.Choice(["json"]), default="json", help="Export format")
344
+ @click.option("--output", "-o", type=click.Path(), help="Output file (default: stdout)")
345
+ @click.option("--status", type=click.Choice(["ok", "error", "unset"]), help="Filter by status")
346
+ @click.option("--limit", default=1000, help="Max traces to export")
347
+ def export(db: str, output_format: str, output: str | None, status: str | None, limit: int) -> None:
348
+ """Export traces and spans to a file."""
349
+ import json
350
+
351
+ from cli.db import get_trace_spans, list_traces
352
+
353
+ rows = list_traces(Path(db), limit=limit, status=status)
354
+
355
+ if not rows:
356
+ console.print("[yellow]No traces to export[/yellow]")
357
+ return
358
+
359
+ traces_data = []
360
+ for row in rows:
361
+ trace_id = str(row["id"])
362
+ spans = get_trace_spans(trace_id, Path(db))
363
+ traces_data.append({
364
+ "id": trace_id,
365
+ "name": row["name"],
366
+ "status": row["status"],
367
+ "session_id": row.get("session_id"),
368
+ "start_time": row.get("start_time"),
369
+ "end_time": row.get("end_time"),
370
+ "metadata": json.loads(row["metadata"]) if row.get("metadata") else None,
371
+ "created_at": row.get("created_at"),
372
+ "spans": spans,
373
+ })
374
+
375
+ export_data = {
376
+ "format": "llm-observatory-export",
377
+ "version": "0.1.1",
378
+ "trace_count": len(traces_data),
379
+ "traces": traces_data,
380
+ }
381
+
382
+ json_str = json.dumps(export_data, indent=2, default=str)
383
+
384
+ if output:
385
+ Path(output).write_text(json_str)
386
+ console.print(f"[green]Exported {len(traces_data)} traces to {output}[/green]")
387
+ else:
388
+ console.print(json_str)
389
+
390
+
391
+ @cli.command()
392
+ @click.option("--endpoint", default="http://localhost:8000", help="API endpoint")
393
+ @click.pass_context
394
+ def status(ctx: click.Context, endpoint: str) -> None:
395
+ """Check the status of the observatory server."""
396
+ import httpx
397
+
398
+ headers: dict[str, str] = {}
399
+ api_key = ctx.obj.get("api_key")
400
+ if api_key:
401
+ headers["Authorization"] = f"Bearer {api_key}"
402
+
403
+ try:
404
+ response = httpx.get(f"{endpoint}/health", headers=headers)
405
+ response.raise_for_status()
406
+
407
+ console.print("[green]Server is healthy![/green]")
408
+ console.print(f"Endpoint: {endpoint}")
409
+ except httpx.HTTPError:
410
+ console.print("[red]Server is not responding[/red]")
411
+
412
+
413
+ if __name__ == "__main__":
414
+ cli()
@@ -0,0 +1,46 @@
1
+ Metadata-Version: 2.4
2
+ Name: magic-lab-observer-cli
3
+ Version: 0.1.1
4
+ Summary: CLI for LLM Observatory
5
+ Project-URL: Homepage, https://github.com/Magic-Lab-Studio/observer
6
+ Project-URL: Repository, https://github.com/Magic-Lab-Studio/observer.git
7
+ Project-URL: Issues, https://github.com/Magic-Lab-Studio/observer/issues
8
+ License-Expression: Apache-2.0
9
+ Requires-Python: >=3.10
10
+ Requires-Dist: click>=8.1.0
11
+ Requires-Dist: httpx>=0.27.0
12
+ Requires-Dist: rich>=13.0.0
13
+ Description-Content-Type: text/markdown
14
+
15
+ # LLM Observatory CLI
16
+
17
+ Command-line interface for LLM Observatory.
18
+
19
+ ## Installation
20
+
21
+ ```bash
22
+ pip install magic-lab-observer-cli
23
+ ```
24
+
25
+ Before the distribution is published, install this directory from the checked-out
26
+ repository with `python -m pip install -e ./cli`.
27
+
28
+ ## Usage
29
+
30
+ ```bash
31
+ # Start the server
32
+ llm-observatory serve
33
+
34
+ # Run an evaluation
35
+ llm-observatory evaluate --trace-id abc123 --evaluator llm_judge --criteria accuracy,safety
36
+
37
+ # Export traces
38
+ llm-observatory export --format json --output traces.json
39
+
40
+ # Check server status
41
+ llm-observatory status
42
+ ```
43
+
44
+ ## License
45
+
46
+ Apache License 2.0
@@ -0,0 +1,7 @@
1
+ cli/__init__.py,sha256=ktHp2A60HiiohlD1aa5ekArVz_BuHEWR1rebup9OvjA,72
2
+ cli/db.py,sha256=5FNEdcLbSou43INuDmwKZcUhNmCS6VTMBh_DShqIhH4,5691
3
+ cli/main.py,sha256=OeChnX9DwaCZTSWI7TBPkAcXoaz1PD9AoEI8sD37XRI,15543
4
+ magic_lab_observer_cli-0.1.1.dist-info/METADATA,sha256=lbl4OjoPI-YACLYQ9PJ7eoJ4Iv_PTXvvYOaJ4V0D4To,1090
5
+ magic_lab_observer_cli-0.1.1.dist-info/WHEEL,sha256=lCkmxWfQsSc9CfIClYeavTdQeEX2toPqufh9gI35EQA,87
6
+ magic_lab_observer_cli-0.1.1.dist-info/entry_points.txt,sha256=bhivRt8UqZcOl_XUoO94J3CdHw4ruCbLnXy5_c-XuFs,49
7
+ magic_lab_observer_cli-0.1.1.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.31.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ llm-observatory = cli.main:cli