ai-agent-inspector 1.0.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.
@@ -0,0 +1,148 @@
1
+ """
2
+ Agent Inspector - Framework-agnostic observability for AI agents.
3
+
4
+ A lightweight, non-blocking tracing system for monitoring and debugging
5
+ AI agent reasoning, tool usage, and execution flow.
6
+
7
+ Example Usage:
8
+ >>> from agent_inspector import trace
9
+ >>>
10
+ >>> with trace.run("my_agent"):
11
+ ... trace.llm(model="gpt-4", prompt="Hello", response="Hi there!")
12
+ ... trace.tool(name="search", args={"q": "flights"}, result="5 flights found")
13
+ ... trace.final(answer="I found 5 flights for you")
14
+
15
+ Example with LangChain:
16
+ >>> from agent_inspector.adapters.langchain_adapter import enable
17
+ >>>
18
+ >>> with enable() as callbacks:
19
+ ... result = agent.run("Search for flights to New York")
20
+ >>> print(result)
21
+ """
22
+
23
+ from importlib.metadata import PackageNotFoundError, version
24
+
25
+ try:
26
+ __version__ = version("ai-agent-inspector")
27
+ except PackageNotFoundError:
28
+ __version__ = "0.0.0.dev"
29
+
30
+ __author__ = "Agent Inspector Team"
31
+ __license__ = "MIT"
32
+
33
+ # Core tracing API
34
+ # Configuration
35
+ from .core.config import (
36
+ Profile,
37
+ TraceConfig,
38
+ get_config,
39
+ set_config,
40
+ )
41
+ from .core.exporters import CompositeExporter
42
+ from .core.interfaces import Exporter, Sampler
43
+ from .core.trace import (
44
+ Trace,
45
+ TraceContext,
46
+ error,
47
+ final,
48
+ get_trace,
49
+ llm,
50
+ memory_read,
51
+ memory_write,
52
+ run,
53
+ set_trace,
54
+ tool,
55
+ )
56
+
57
+ # LangChain adapter
58
+ try:
59
+ from .adapters.langchain_adapter import enable as enable_langchain
60
+ except ImportError:
61
+ # LangChain is optional
62
+ enable_langchain = None
63
+
64
+ # API server
65
+ try:
66
+ from .api.main import get_api_server, run_server
67
+ except ImportError:
68
+ # FastAPI is optional
69
+ run_server = None
70
+ get_api_server = None
71
+
72
+ # Event types
73
+ from .core.events import (
74
+ BaseEvent,
75
+ ErrorEvent,
76
+ EventStatus,
77
+ EventType,
78
+ FinalAnswerEvent,
79
+ LLMCallEvent,
80
+ MemoryReadEvent,
81
+ MemoryWriteEvent,
82
+ RunEndEvent,
83
+ RunStartEvent,
84
+ ToolCallEvent,
85
+ )
86
+
87
+ __all__ = [
88
+ # Version
89
+ "__version__",
90
+ # Core tracing
91
+ "Trace",
92
+ "TraceContext",
93
+ "trace", # Convenience alias for get_trace()
94
+ "run",
95
+ "llm",
96
+ "tool",
97
+ "memory_read",
98
+ "memory_write",
99
+ "error",
100
+ "final",
101
+ "get_trace",
102
+ "set_trace",
103
+ # Configuration
104
+ "TraceConfig",
105
+ "Profile",
106
+ "get_config",
107
+ "set_config",
108
+ # Extensibility
109
+ "Exporter",
110
+ "Sampler",
111
+ "CompositeExporter",
112
+ # Event types
113
+ "EventType",
114
+ "EventStatus",
115
+ "BaseEvent",
116
+ "RunStartEvent",
117
+ "RunEndEvent",
118
+ "LLMCallEvent",
119
+ "ToolCallEvent",
120
+ "MemoryReadEvent",
121
+ "MemoryWriteEvent",
122
+ "ErrorEvent",
123
+ "FinalAnswerEvent",
124
+ # Adapters (optional)
125
+ "enable_langchain",
126
+ # API server (optional)
127
+ "run_server",
128
+ "get_api_server",
129
+ ]
130
+
131
+
132
+ # Convenience property for global trace instance
133
+ class _GlobalTrace:
134
+ """Convenience wrapper for global trace instance."""
135
+
136
+ def __getattr__(self, name):
137
+ """Proxy all attribute access to global trace instance."""
138
+ global_trace = get_trace()
139
+ return getattr(global_trace, name)
140
+
141
+
142
+ trace = _GlobalTrace()
143
+ """Global trace instance for convenience usage.
144
+
145
+ Example:
146
+ >>> trace.run("my_agent")
147
+ >>> trace.llm(model="gpt-4", prompt="...", response="...")
148
+ """
agent_inspector/cli.py ADDED
@@ -0,0 +1,532 @@
1
+ """
2
+ Command-line interface for Agent Inspector.
3
+
4
+ Provides commands for:
5
+ - Starting the API server
6
+ - Viewing database statistics
7
+ - Pruning old trace data
8
+ - Exporting runs to JSON
9
+ - Managing configuration
10
+ """
11
+
12
+ import argparse
13
+ import json
14
+ import logging
15
+ import sys
16
+ from typing import Optional
17
+
18
+ from . import __version__
19
+ from .api.main import run_server
20
+ from .core.config import Profile, TraceConfig, get_config, set_config
21
+
22
+
23
+ def setup_logging(log_level: str = "INFO", log_file: Optional[str] = None):
24
+ """
25
+ Setup logging configuration.
26
+
27
+ Args:
28
+ log_level: Logging level (DEBUG, INFO, WARNING, ERROR).
29
+ log_file: Optional path to log file. If None, logs to stdout.
30
+ """
31
+ logging.basicConfig(
32
+ level=getattr(logging, log_level.upper()),
33
+ format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
34
+ filename=log_file,
35
+ )
36
+
37
+
38
+ def cmd_server(args):
39
+ """Start the API server."""
40
+ print("🚀 Starting Agent Inspector API server...")
41
+
42
+ # Setup logging
43
+ setup_logging(log_level=args.log_level, log_file=args.log_file)
44
+
45
+ # Get or create configuration
46
+ config = get_config()
47
+
48
+ # Override with CLI arguments
49
+ if args.host:
50
+ config.api_host = args.host
51
+ if args.port:
52
+ config.api_port = args.port
53
+
54
+ # Set the config
55
+ set_config(config)
56
+
57
+ # Start server
58
+ print(f"📍 Server running on http://{config.api_host}:{config.api_port}")
59
+ print(f"🌐 UI available at http://{config.api_host}:{config.api_port}/ui")
60
+ print(f"📚 API docs at http://{config.api_host}:{config.api_port}/docs")
61
+ print("\nPress Ctrl+C to stop the server\n")
62
+
63
+ run_server(host=config.api_host, port=config.api_port)
64
+
65
+
66
+ def cmd_stats(args):
67
+ """View database statistics."""
68
+ from .storage.database import Database
69
+
70
+ print("📊 Agent Inspector Statistics\n")
71
+
72
+ # Setup logging
73
+ setup_logging(log_level="WARNING")
74
+
75
+ # Get configuration
76
+ config = get_config()
77
+
78
+ # Initialize database
79
+ db = Database(config)
80
+ db.initialize()
81
+
82
+ # Get stats
83
+ stats = db.get_stats()
84
+
85
+ if not stats:
86
+ print("❌ No statistics available")
87
+ return 1
88
+
89
+ # Display stats
90
+ print(f"Total Runs: {stats.get('total_runs', 0)}")
91
+ print(f" - Running: {stats.get('running_runs', 0)}")
92
+ print(f" - Completed: {stats.get('completed_runs', 0)}")
93
+ print(f" - Failed: {stats.get('failed_runs', 0)}")
94
+ print(f"\nTotal Steps (Events): {stats.get('total_steps', 0)}")
95
+ print(f"Database Size: {stats.get('db_size_bytes', 0):,} bytes")
96
+ print(f"\nRecent Activity (24h): {stats.get('recent_runs_24h', 0)} runs")
97
+
98
+ return 0
99
+
100
+
101
+ def cmd_prune(args):
102
+ """Prune old trace data."""
103
+ from .storage.database import Database
104
+
105
+ print("🧹 Pruning old trace data...")
106
+
107
+ # Setup logging
108
+ setup_logging(log_level=args.log_level or "INFO")
109
+
110
+ # Get configuration
111
+ config = get_config()
112
+
113
+ # Override with CLI arguments
114
+ if args.retention_days is not None:
115
+ config.retention_days = args.retention_days
116
+
117
+ # Initialize database
118
+ db = Database(config)
119
+ db.initialize()
120
+
121
+ # Prune old runs
122
+ deleted_count = db.prune_old_runs(retention_days=config.retention_days)
123
+
124
+ if deleted_count > 0:
125
+ print(f"✅ Pruned {deleted_count} old runs")
126
+
127
+ # Optionally vacuum to reclaim space
128
+ if args.vacuum:
129
+ print("💾 Running VACUUM to reclaim disk space...")
130
+ if db.vacuum():
131
+ print("✅ VACUUM completed")
132
+ else:
133
+ print("⚠️ VACUUM failed")
134
+ else:
135
+ print("ℹ️ No runs to prune")
136
+
137
+ return 0
138
+
139
+
140
+ def cmd_vacuum(args):
141
+ """Run VACUUM to reclaim disk space."""
142
+ from .storage.database import Database
143
+
144
+ print("💾 Running VACUUM to reclaim disk space...")
145
+
146
+ # Setup logging
147
+ setup_logging(log_level="WARNING")
148
+
149
+ # Get configuration
150
+ config = get_config()
151
+
152
+ # Initialize database
153
+ db = Database(config)
154
+ db.initialize()
155
+
156
+ # Run vacuum
157
+ if db.vacuum():
158
+ print("✅ VACUUM completed successfully")
159
+ return 0
160
+ else:
161
+ print("❌ VACUUM failed")
162
+ return 1
163
+
164
+
165
+ def cmd_backup(args):
166
+ """Create a database backup."""
167
+ from .storage.database import Database
168
+
169
+ print(f"💾 Creating backup to {args.backup_path}...")
170
+
171
+ # Setup logging
172
+ setup_logging(log_level="INFO")
173
+
174
+ # Get configuration
175
+ config = get_config()
176
+
177
+ # Initialize database
178
+ db = Database(config)
179
+ db.initialize()
180
+
181
+ # Create backup
182
+ if db.backup(args.backup_path):
183
+ print(f"✅ Backup created at {args.backup_path}")
184
+ return 0
185
+ else:
186
+ print("❌ Backup failed")
187
+ return 1
188
+
189
+
190
+ def cmd_export(args):
191
+ """Export run(s) to JSON file or stdout."""
192
+ from .processing.pipeline import ProcessingPipeline
193
+ from .storage.database import Database
194
+
195
+ if not getattr(args, "all_runs", False) and not getattr(args, "run_id", None):
196
+ print("Error: provide run_id or use --all", file=sys.stderr)
197
+ return 1
198
+
199
+ setup_logging(log_level="INFO")
200
+ config = get_config()
201
+ db = Database(config)
202
+ db.initialize()
203
+ pipeline = ProcessingPipeline(config)
204
+
205
+ def export_one(run_id: str) -> Optional[dict]:
206
+ run = db.get_run(run_id)
207
+ if not run:
208
+ print(f"Run {run_id} not found", file=sys.stderr)
209
+ return None
210
+ timeline = db.get_run_timeline(run_id=run_id, include_data=True)
211
+ for event in timeline:
212
+ if event.get("data"):
213
+ try:
214
+ event["data"] = pipeline.reverse(event["data"])
215
+ except Exception:
216
+ event["data"] = None
217
+ return {"run": dict(run), "timeline": timeline}
218
+
219
+ if getattr(args, "all_runs", False):
220
+ runs = db.list_runs(limit=args.limit or 1000, offset=0)
221
+ payload = []
222
+ for r in runs:
223
+ rid = r.get("id")
224
+ if rid:
225
+ one = export_one(rid)
226
+ if one:
227
+ payload.append(one)
228
+ data = {"runs": payload, "total": len(payload)}
229
+ else:
230
+ one = export_one(args.run_id)
231
+ if not one:
232
+ return 1
233
+ data = one
234
+
235
+ out = args.output
236
+ json_str = json.dumps(data, indent=2, default=str)
237
+ if out:
238
+ with open(out, "w") as f:
239
+ f.write(json_str)
240
+ print(f"✅ Exported to {out}")
241
+ else:
242
+ print(json_str)
243
+ return 0
244
+
245
+
246
+ def cmd_config(args):
247
+ """View or set configuration."""
248
+ config = get_config()
249
+
250
+ if args.show:
251
+ # Show current configuration
252
+ print("⚙️ Current Configuration\n")
253
+ print(config.to_json())
254
+ elif args.profile:
255
+ # Set profile
256
+ try:
257
+ profile = Profile(args.profile.lower())
258
+
259
+ if profile == Profile.PRODUCTION:
260
+ new_config = TraceConfig.production()
261
+ elif profile == Profile.DEVELOPMENT:
262
+ new_config = TraceConfig.development()
263
+ elif profile == Profile.DEBUG:
264
+ new_config = TraceConfig.debug()
265
+ else:
266
+ print(f"❌ Unknown profile: {args.profile}")
267
+ return 1
268
+
269
+ set_config(new_config)
270
+ print(f"✅ Configuration set to {profile.value} profile")
271
+ return 0
272
+ except ValueError as e:
273
+ print(f"❌ Invalid profile: {e}")
274
+ return 1
275
+ else:
276
+ # Show brief configuration
277
+ print("⚙️ Agent Inspector Configuration\n")
278
+ print(f"Sample Rate: {config.sample_rate * 100:.1f}%")
279
+ print(f"Only on Error: {config.only_on_error}")
280
+ print(f"Encryption: {'Enabled' if config.encryption_enabled else 'Disabled'}")
281
+ print(f"Compression: {'Enabled' if config.compression_enabled else 'Disabled'}")
282
+ print(f"API Host: {config.api_host}:{config.api_port}")
283
+ print(f"Database: {config.db_path}")
284
+
285
+ return 0
286
+
287
+
288
+ def cmd_init(args):
289
+ """Initialize Agent Inspector."""
290
+ print("🔧 Initializing Agent Inspector...")
291
+
292
+ # Create default configuration
293
+ config = TraceConfig()
294
+
295
+ # Override with arguments
296
+ if args.profile:
297
+ try:
298
+ profile = Profile(args.profile.lower())
299
+ if profile == Profile.PRODUCTION:
300
+ config = TraceConfig.production()
301
+ elif profile == Profile.DEVELOPMENT:
302
+ config = TraceConfig.development()
303
+ elif profile == Profile.DEBUG:
304
+ config = TraceConfig.debug()
305
+ except ValueError as e:
306
+ print(f"❌ Invalid profile: {e}")
307
+ return 1
308
+
309
+ # Initialize database
310
+ from .storage.database import Database
311
+
312
+ db = Database(config)
313
+ db.initialize()
314
+
315
+ print("✅ Agent Inspector initialized!")
316
+ print(f"\n📍 Database: {config.db_path}")
317
+ print(f"📊 Sample Rate: {config.sample_rate * 100:.1f}%")
318
+ print(f"🔒 Encryption: {'Enabled' if config.encryption_enabled else 'Disabled'}")
319
+
320
+ print("\n💡 Next steps:")
321
+ print(" - Start API server: agent-inspector server")
322
+ print(" - Run examples: python examples/basic_tracing.py")
323
+ print(" - View UI: http://localhost:8000/ui")
324
+
325
+ return 0
326
+
327
+
328
+ def main():
329
+ """Main entry point for CLI."""
330
+ parser = argparse.ArgumentParser(
331
+ prog="agent-inspector",
332
+ description="Framework-agnostic observability for AI agents",
333
+ formatter_class=argparse.RawDescriptionHelpFormatter,
334
+ epilog="""
335
+ Examples:
336
+ # Start API server
337
+ agent-inspector server
338
+
339
+ # Start server on custom port
340
+ agent-inspector server --port 8080
341
+
342
+ # View statistics
343
+ agent-inspector stats
344
+
345
+ # Prune data older than 30 days
346
+ agent-inspector prune --retention-days 30
347
+
348
+ # Set development profile
349
+ agent-inspector config --profile development
350
+
351
+ # Initialize with debug profile
352
+ agent-inspector init --profile debug
353
+ """,
354
+ )
355
+
356
+ parser.add_argument(
357
+ "--version",
358
+ action="version",
359
+ version=f"%(prog)s {__version__}",
360
+ )
361
+
362
+ subparsers = parser.add_subparsers(dest="command", help="Available commands")
363
+
364
+ # Server command
365
+ server_parser = subparsers.add_parser(
366
+ "server",
367
+ help="Start the API server",
368
+ description="Start the FastAPI server for serving trace data and UI",
369
+ )
370
+ server_parser.add_argument(
371
+ "--host",
372
+ type=str,
373
+ help="Host to bind to (default: 127.0.0.1)",
374
+ )
375
+ server_parser.add_argument(
376
+ "--port",
377
+ type=int,
378
+ help="Port to bind to (default: 8000)",
379
+ )
380
+ server_parser.add_argument(
381
+ "--log-level",
382
+ type=str,
383
+ choices=["DEBUG", "INFO", "WARNING", "ERROR"],
384
+ default="INFO",
385
+ help="Log level (default: INFO)",
386
+ )
387
+ server_parser.add_argument(
388
+ "--log-file",
389
+ type=str,
390
+ help="Path to log file (default: stdout)",
391
+ )
392
+
393
+ # Stats command
394
+ _stats_parser = subparsers.add_parser(
395
+ "stats",
396
+ help="View database statistics",
397
+ description="Display statistics about stored trace data",
398
+ )
399
+
400
+ # Prune command
401
+ prune_parser = subparsers.add_parser(
402
+ "prune",
403
+ help="Prune old trace data",
404
+ description="Delete trace data older than the retention period",
405
+ )
406
+ prune_parser.add_argument(
407
+ "--retention-days",
408
+ type=int,
409
+ help="Retention period in days (default: from config)",
410
+ )
411
+ prune_parser.add_argument(
412
+ "--vacuum",
413
+ action="store_true",
414
+ help="Run VACUUM after pruning to reclaim disk space",
415
+ )
416
+ prune_parser.add_argument(
417
+ "--log-level",
418
+ type=str,
419
+ choices=["DEBUG", "INFO", "WARNING", "ERROR"],
420
+ help="Log level",
421
+ )
422
+
423
+ # Vacuum command
424
+ _vacuum_parser = subparsers.add_parser(
425
+ "vacuum",
426
+ help="Run VACUUM to reclaim disk space",
427
+ description="Run SQLite VACUUM to reclaim disk space",
428
+ )
429
+
430
+ # Backup command
431
+ backup_parser = subparsers.add_parser(
432
+ "backup",
433
+ help="Create database backup",
434
+ description="Create a backup of the SQLite database",
435
+ )
436
+ backup_parser.add_argument(
437
+ "backup_path",
438
+ type=str,
439
+ help="Path where backup should be saved",
440
+ )
441
+
442
+ # Export command
443
+ export_parser = subparsers.add_parser(
444
+ "export",
445
+ help="Export run(s) to JSON",
446
+ description="Export a run or all runs to JSON (run metadata + timeline with decoded event data)",
447
+ )
448
+ export_parser.add_argument(
449
+ "run_id",
450
+ nargs="?",
451
+ type=str,
452
+ help="Run ID to export (omit if using --all)",
453
+ )
454
+ export_parser.add_argument(
455
+ "--all",
456
+ dest="all_runs",
457
+ action="store_true",
458
+ help="Export all runs",
459
+ )
460
+ export_parser.add_argument(
461
+ "--limit",
462
+ type=int,
463
+ default=1000,
464
+ help="Max runs to export when using --all (default: 1000)",
465
+ )
466
+ export_parser.add_argument(
467
+ "--output",
468
+ "-o",
469
+ type=str,
470
+ default=None,
471
+ help="Output file path (default: stdout)",
472
+ )
473
+
474
+ # Config command
475
+ config_parser = subparsers.add_parser(
476
+ "config",
477
+ help="View or set configuration",
478
+ description="View current configuration or set a profile preset",
479
+ )
480
+ config_parser.add_argument(
481
+ "--show",
482
+ action="store_true",
483
+ help="Show full configuration",
484
+ )
485
+ config_parser.add_argument(
486
+ "--profile",
487
+ type=str,
488
+ choices=["production", "development", "debug"],
489
+ help="Set configuration profile (production, development, debug)",
490
+ )
491
+
492
+ # Init command
493
+ init_parser = subparsers.add_parser(
494
+ "init",
495
+ help="Initialize Agent Inspector",
496
+ description="Initialize Agent Inspector with default configuration",
497
+ )
498
+ init_parser.add_argument(
499
+ "--profile",
500
+ type=str,
501
+ choices=["production", "development", "debug"],
502
+ help="Configuration profile to use",
503
+ )
504
+
505
+ # Parse arguments
506
+ args = parser.parse_args()
507
+
508
+ # Execute command
509
+ if args.command == "server":
510
+ return cmd_server(args)
511
+ elif args.command == "stats":
512
+ return cmd_stats(args)
513
+ elif args.command == "prune":
514
+ return cmd_prune(args)
515
+ elif args.command == "vacuum":
516
+ return cmd_vacuum(args)
517
+ elif args.command == "backup":
518
+ return cmd_backup(args)
519
+ elif args.command == "export":
520
+ return cmd_export(args)
521
+ elif args.command == "config":
522
+ return cmd_config(args)
523
+ elif args.command == "init":
524
+ return cmd_init(args)
525
+ else:
526
+ # No command specified, show help
527
+ parser.print_help()
528
+ return 0
529
+
530
+
531
+ if __name__ == "__main__":
532
+ sys.exit(main() or 0)