agentlens-py 0.1.0__tar.gz

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,170 @@
1
+ # ============================================================
2
+ # Python
3
+ # ============================================================
4
+ __pycache__/
5
+ *.py[cod]
6
+ *$py.class
7
+ *.so
8
+ *.egg
9
+ *.egg-info/
10
+ dist/
11
+ build/
12
+ eggs/
13
+ parts/
14
+ var/
15
+ sdist/
16
+ develop-eggs/
17
+ .installed.cfg
18
+ lib/
19
+ lib64/
20
+ .eggs/
21
+
22
+ # Virtual environments
23
+ .venv/
24
+ venv/
25
+ ENV/
26
+ env/
27
+ .env
28
+ .env.local
29
+ .env.*.local
30
+
31
+ # pytest / coverage
32
+ .pytest_cache/
33
+ .coverage
34
+ .coverage.*
35
+ htmlcov/
36
+ coverage.xml
37
+ *.cover
38
+ .hypothesis/
39
+
40
+ # mypy / pyright
41
+ .mypy_cache/
42
+ .dmypy.json
43
+ dmypy.json
44
+ .pyright/
45
+
46
+ # ruff / flake8
47
+ .ruff_cache/
48
+
49
+ # Distribution / packaging
50
+ MANIFEST
51
+
52
+ # ============================================================
53
+ # Node / Next.js
54
+ # ============================================================
55
+ node_modules/
56
+ .next/
57
+ out/
58
+ .nuxt/
59
+ .cache/
60
+ .parcel-cache/
61
+ .turbo/
62
+
63
+ # Build output
64
+ dist/
65
+ build/
66
+
67
+ # Logs
68
+ npm-debug.log*
69
+ yarn-debug.log*
70
+ yarn-error.log*
71
+ pnpm-debug.log*
72
+ lerna-debug.log*
73
+
74
+ # Runtime data
75
+ pids/
76
+ *.pid
77
+ *.seed
78
+ *.pid.lock
79
+
80
+ # TypeScript
81
+ *.tsbuildinfo
82
+ next-env.d.ts
83
+
84
+ # ============================================================
85
+ # Docker
86
+ # ============================================================
87
+ .dockerignore
88
+
89
+ # ============================================================
90
+ # OS / Editor
91
+ # ============================================================
92
+ .DS_Store
93
+ Thumbs.db
94
+ *.swp
95
+ *.swo
96
+ *~
97
+ .idea/
98
+ .vscode/
99
+ *.sublime-project
100
+ *.sublime-workspace
101
+
102
+ # ============================================================
103
+ # Secrets / credentials (never commit these)
104
+ # ============================================================
105
+ .env
106
+ .env.local
107
+ .env.development.local
108
+ .env.test.local
109
+ .env.production.local
110
+ *.pem
111
+ *.key
112
+ secrets/
113
+
114
+ # ============================================================
115
+ # Alembic
116
+ # ============================================================
117
+ # Keep migrations tracked; ignore only generated artefacts
118
+ apps/api/migrations/__pycache__/
119
+ apps/api/migrations/versions/__pycache__/
120
+
121
+ # ============================================================
122
+ # Misc
123
+ # ============================================================
124
+ *.log
125
+ *.tmp
126
+ *.bak
127
+
128
+ # ============================================================
129
+ # Pre-launch release cleanup additions
130
+ # ============================================================
131
+ .Python
132
+ downloads/
133
+ wheels/
134
+ *.pyo
135
+ .env/
136
+ apps/api/.env
137
+ apps/web/.env
138
+ apps/web/.env.local
139
+ secrets.txt
140
+ apps/web/.next/
141
+ apps/web/out/
142
+ apps/web/node_modules/
143
+ *.code-workspace
144
+ *.tar
145
+ logs/
146
+ scratch_test.py
147
+ ws_test_delay.py
148
+ test_trace.py
149
+
150
+ # Internal planning documents
151
+ ANTIGRAVITY_PROMPT.md
152
+ agentlens_project_handover_summary.md
153
+ *_handover_*.md
154
+
155
+ # Manual test scripts
156
+ test_agent.py
157
+ verify_dashboard_data.py
158
+ test_trace.py
159
+
160
+ # Kiro AI IDE specs (internal)
161
+ .kiro/
162
+
163
+ # Editor
164
+ .vscode/
165
+
166
+ # Hypothesis test data
167
+ apps/api/.hypothesis/
168
+ apps/api/tmp/
169
+ apps/api/unicode_data/
170
+
@@ -0,0 +1,11 @@
1
+ Metadata-Version: 2.4
2
+ Name: agentlens-py
3
+ Version: 0.1.0
4
+ Summary: AgentLens Python SDK — zero-overhead observability for AI agents
5
+ Requires-Python: >=3.9
6
+ Requires-Dist: httpx==0.28.1
7
+ Requires-Dist: structlog==25.5.0
8
+ Provides-Extra: dev
9
+ Requires-Dist: hypothesis==6.152.9; extra == 'dev'
10
+ Requires-Dist: pytest-asyncio==1.3.0; extra == 'dev'
11
+ Requires-Dist: pytest==9.0.3; extra == 'dev'
@@ -0,0 +1,120 @@
1
+ # AgentLens Python SDK
2
+
3
+ The official Python client for AgentLens. Easily trace agent execution steps, nested tool calls, and LLM completions with non-blocking, asynchronous ingestion.
4
+
5
+ ## Key Features
6
+
7
+ - **Micro-Batched Background Ingestion**: Uses a thread-safe daemon worker (`SpanQueue` wrapping a safe `queue.Queue`) to buffer and flush spans concurrently without delaying agent execution.
8
+ - **Zero-Dependency Exception Guard**: The SDK never propagates internal networking or API exceptions, ensuring your production agent runs remain robust.
9
+ - **Automatic Cost & Token Estimation**: Immediate calculations of token consumption and pricing for popular OpenAI and Anthropic models.
10
+ - **Auto-Instrumentation**: Monkey-patches OpenAI and Anthropic clients to capture inputs, outputs, tokens, and streaming responses out-of-the-box.
11
+
12
+ ---
13
+
14
+ ## Installation
15
+
16
+ Install the package via `pip` (or `uv` / `poetry`):
17
+
18
+ ```bash
19
+ pip install agentlens-py
20
+ ```
21
+
22
+ > [!NOTE]
23
+ > Note that the package is installed via `agentlens-py`, but the Python import name remains `import agentlens as al`.
24
+
25
+ ---
26
+
27
+ ## Quick Start (2-Line Integration)
28
+
29
+ Instrument your application to capture all OpenAI completions automatically:
30
+
31
+ ```python
32
+ import agentlens as al
33
+
34
+ # Initialize the global client and instrument the OpenAI SDK
35
+ al.init(api_key="al_live_YOUR_API_KEY_HERE", base_url="http://localhost:8000")
36
+ al.instrument_openai()
37
+ ```
38
+
39
+ ---
40
+
41
+ ## Detailed Usage
42
+
43
+ ### Decorators & Context Managers
44
+
45
+ Trace custom Python functions or specific logical blocks inside your agents:
46
+
47
+ ```python
48
+ import agentlens as al
49
+
50
+ # 1. Trace a high-level agent loop
51
+ @al.trace(name="Customer Assistant Agent")
52
+ def run_assistant(user_query: str):
53
+ print("Agent started...")
54
+
55
+ # 2. Trace tool calls or sub-steps using context managers
56
+ with al.span(name="Database Lookup", span_type="tool") as s:
57
+ s.set_metadata("query", user_query)
58
+ result = db_search(user_query)
59
+ s.set_output({"results_count": len(result)})
60
+
61
+ return result
62
+ ```
63
+
64
+ ### Auto-Instrumentation Examples
65
+
66
+ #### OpenAI (Both Streaming & Non-Streaming)
67
+
68
+ ```python
69
+ import openai
70
+ import agentlens as al
71
+
72
+ al.init(api_key="al_live_...")
73
+ al.instrument_openai()
74
+
75
+ client = openai.OpenAI()
76
+
77
+ # Spans are automatically created for completions, including token counts and costs
78
+ response = client.chat.completions.create(
79
+ model="gpt-4o",
80
+ messages=[{"role": "user", "content": "Explain quantizing neural networks."}]
81
+ )
82
+ ```
83
+
84
+ #### Anthropic (Both Streaming & Non-Streaming)
85
+
86
+ ```python
87
+ import anthropic
88
+ import agentlens as al
89
+
90
+ al.init(api_key="al_live_...")
91
+ al.instrument_anthropic()
92
+
93
+ client = anthropic.Anthropic()
94
+
95
+ response = client.messages.create(
96
+ model="claude-3-5-sonnet-20241022",
97
+ max_tokens=1000,
98
+ messages=[{"role": "user", "content": "What is reinforcement learning?"}]
99
+ )
100
+ ```
101
+
102
+ ---
103
+
104
+ ## Public API Reference
105
+
106
+ ### `al.init(api_key: str, base_url: str = "http://localhost:8000", debug: bool = False, enabled: bool = True) -> None`
107
+ Configures the global AgentLens client, spins up the thread-safe background ingestion worker, and registers an `atexit` hook to flush remaining spans on application termination.
108
+ - `api_key`: Your active AgentLens project API key (prefixed with `al_live_`).
109
+ - `base_url`: Endpoint of your FastAPI backend.
110
+ - `debug`: Enable internal logging for SDK debugging.
111
+ - `enabled`: If set to `False`, all tracing operations, monkey-patches, and decorators gracefully fallback to no-ops.
112
+
113
+ ### `al.trace(name: str | None = None, tags: list[str] | None = None)`
114
+ Decorator to declare a new root trace context. Propagates the trace context down the execution tree.
115
+
116
+ ### `al.span(name: str, span_type: str = "custom")`
117
+ Context manager to record a logical sub-step inside an active trace context. Supports updating metadata and final outputs.
118
+
119
+ ### `al.flush(timeout: float = 5.0) -> None`
120
+ Manually blocks and flushes any enqueued spans to the API backend up to the specified timeout. Excellent for short-running serverless function environments.
@@ -0,0 +1,62 @@
1
+ """AgentLens Python SDK — zero-overhead observability for AI agents.
2
+ """
3
+
4
+ from __future__ import annotations
5
+
6
+ import atexit
7
+ import logging
8
+ from typing import Optional
9
+
10
+ from agentlens.client import AgentLensClient
11
+ from agentlens.decorators import span, trace
12
+ from agentlens.instruments.anthropic import instrument_anthropic
13
+ from agentlens.instruments.openai import instrument_openai
14
+
15
+ __all__ = [
16
+ "init",
17
+ "instrument_openai",
18
+ "instrument_anthropic",
19
+ "trace",
20
+ "span",
21
+ "flush",
22
+ ]
23
+
24
+ _global_client: Optional[AgentLensClient] = None
25
+ logger = logging.getLogger("agentlens")
26
+
27
+
28
+ def init(
29
+ api_key: str,
30
+ base_url: str = "http://localhost:8000",
31
+ debug: bool = False,
32
+ enabled: bool = True,
33
+ ) -> AgentLensClient:
34
+ """Initialize the global AgentLens client."""
35
+ global _global_client
36
+
37
+ if _global_client is not None:
38
+ _global_client.stop()
39
+
40
+ _global_client = AgentLensClient(
41
+ api_key=api_key,
42
+ base_url=base_url,
43
+ debug=debug,
44
+ enabled=enabled,
45
+ )
46
+
47
+ # Register exit handler to flush queue on shutdown
48
+ if enabled:
49
+ atexit.register(flush)
50
+
51
+ return _global_client
52
+
53
+
54
+ def flush(timeout: float = 5.0) -> None:
55
+ """Flush all currently buffered spans in the global client."""
56
+ global _global_client
57
+ if _global_client is not None and _global_client.enabled:
58
+ try:
59
+ _global_client.flush(timeout=timeout)
60
+ except Exception as e:
61
+ if _global_client.debug:
62
+ logger.debug(f"Failed to flush spans: {e}", exc_info=True)
@@ -0,0 +1,137 @@
1
+ """AgentLens SDK Client for background batch span ingestion.
2
+ """
3
+
4
+ from __future__ import annotations
5
+
6
+ import atexit
7
+ import logging
8
+ import queue
9
+ import threading
10
+ import time
11
+ from typing import Any, Dict, List, Optional
12
+
13
+ import httpx
14
+
15
+ logger = logging.getLogger("agentlens")
16
+
17
+
18
+ class AgentLensClient:
19
+ def __init__(
20
+ self,
21
+ api_key: str,
22
+ base_url: str = "http://localhost:8000",
23
+ debug: bool = False,
24
+ enabled: bool = True,
25
+ ):
26
+ self.api_key = api_key
27
+ self.base_url = base_url.rstrip("/")
28
+ self.debug = debug
29
+ self.enabled = enabled
30
+
31
+ self._queue: queue.Queue[Dict[str, Any]] = queue.Queue(maxsize=10000)
32
+ self._stop_event = threading.Event()
33
+ self._worker_thread: Optional[threading.Thread] = None
34
+
35
+ if self.enabled:
36
+ self.start()
37
+
38
+ def start(self):
39
+ if self._worker_thread is not None and self._worker_thread.is_alive():
40
+ return
41
+ self._stop_event.clear()
42
+ self._worker_thread = threading.Thread(target=self._worker, daemon=True)
43
+ self._worker_thread.start()
44
+
45
+ def enqueue(self, item: Dict[str, Any]):
46
+ if not self.enabled:
47
+ return
48
+ try:
49
+ self._queue.put_nowait(item)
50
+ except queue.Full:
51
+ logger.warning("AgentLens queue full. Draining one item to make space.")
52
+ try:
53
+ self._queue.get_nowait()
54
+ self._queue.task_done()
55
+ except (queue.Empty, ValueError):
56
+ pass
57
+ try:
58
+ self._queue.put_nowait(item)
59
+ except queue.Full:
60
+ pass # Fail-safe
61
+
62
+ def _safe_enqueue(self, item: Dict[str, Any]):
63
+ try:
64
+ self.enqueue(item)
65
+ except Exception as e:
66
+ if self.debug:
67
+ logger.debug(f"Failed to enqueue span: {e}", exc_info=True)
68
+
69
+ def _send_batch(self, batch: List[Dict[str, Any]]):
70
+ if not batch:
71
+ return
72
+ try:
73
+ url = f"{self.base_url}/v1/ingest"
74
+ headers = {
75
+ "Authorization": f"Bearer {self.api_key}",
76
+ "Content-Type": "application/json",
77
+ }
78
+ payload = {"spans": batch}
79
+
80
+ # Sync request since we are on background worker thread
81
+ with httpx.Client(timeout=10.0) as client:
82
+ resp = client.post(url, json=payload, headers=headers)
83
+ if resp.status_code != 200:
84
+ if self.debug:
85
+ logger.debug(
86
+ f"Ingest failed with status {resp.status_code}: {resp.text}"
87
+ )
88
+ except Exception as e:
89
+ if self.debug:
90
+ logger.debug(f"Error sending batch to AgentLens: {e}", exc_info=True)
91
+
92
+ def _worker(self):
93
+ while not self._stop_event.is_set():
94
+ batch = []
95
+ start_time = time.time()
96
+ while len(batch) < 100:
97
+ elapsed = time.time() - start_time
98
+ if elapsed >= 0.5:
99
+ break
100
+ timeout = 0.5 - elapsed
101
+ try:
102
+ item = self._queue.get(timeout=timeout)
103
+ batch.append(item)
104
+ except queue.Empty:
105
+ break
106
+
107
+ if batch:
108
+ self._send_batch(batch)
109
+ for _ in range(len(batch)):
110
+ self._queue.task_done()
111
+ else:
112
+ time.sleep(0.05)
113
+
114
+ def flush(self, timeout: float = 5.0):
115
+ start_time = time.time()
116
+ while not self._queue.empty():
117
+ if time.time() - start_time > timeout:
118
+ break
119
+ batch = []
120
+ while len(batch) < 100:
121
+ try:
122
+ item = self._queue.get_nowait()
123
+ batch.append(item)
124
+ except queue.Empty:
125
+ break
126
+ if batch:
127
+ self._send_batch(batch)
128
+ for _ in range(len(batch)):
129
+ self._queue.task_done()
130
+ else:
131
+ break
132
+
133
+ def stop(self, timeout: float = 5.0):
134
+ self._stop_event.set()
135
+ self.flush(timeout=timeout)
136
+ if self._worker_thread is not None:
137
+ self._worker_thread.join(timeout=timeout)
@@ -0,0 +1,30 @@
1
+ """ContextVar-based trace/span ID propagation.
2
+
3
+ These vars are set by the @trace decorator and span() context manager so that
4
+ nested calls automatically inherit the current trace and span context without
5
+ any explicit passing.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from contextvars import ContextVar
11
+ from typing import Optional
12
+
13
+ _current_trace_id: ContextVar[Optional[str]] = ContextVar("_current_trace_id", default=None)
14
+ _current_span_id: ContextVar[Optional[str]] = ContextVar("_current_span_id", default=None)
15
+
16
+
17
+ def get_trace_id() -> Optional[str]:
18
+ return _current_trace_id.get()
19
+
20
+
21
+ def get_span_id() -> Optional[str]:
22
+ return _current_span_id.get()
23
+
24
+
25
+ def set_trace_id(value: Optional[str]):
26
+ return _current_trace_id.set(value)
27
+
28
+
29
+ def set_span_id(value: Optional[str]):
30
+ return _current_span_id.set(value)