agentpulse-py 0.2.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,8 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ *.egg-info/
4
+ dist/
5
+ build/
6
+ .env
7
+ .venv/
8
+ venv/
@@ -0,0 +1,345 @@
1
+ Metadata-Version: 2.4
2
+ Name: agentpulse-py
3
+ Version: 0.2.0
4
+ Summary: Lightweight observability SDK for AI agents — non-blocking run tracking with batching and retries
5
+ Project-URL: Homepage, https://github.com/your-org/agenttrace-py
6
+ Project-URL: Documentation, https://docs.agenttrace.dev
7
+ Project-URL: Bug Tracker, https://github.com/your-org/agenttrace-py/issues
8
+ License: MIT
9
+ Keywords: agents,ai,llm,monitoring,observability,tracing
10
+ Classifier: Development Status :: 4 - Beta
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.8
15
+ Classifier: Programming Language :: Python :: 3.9
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Topic :: Software Development :: Libraries
20
+ Classifier: Topic :: System :: Monitoring
21
+ Classifier: Typing :: Typed
22
+ Requires-Python: >=3.8
23
+ Provides-Extra: dev
24
+ Requires-Dist: build; extra == 'dev'
25
+ Requires-Dist: pytest; extra == 'dev'
26
+ Requires-Dist: twine; extra == 'dev'
27
+ Description-Content-Type: text/markdown
28
+
29
+ # agenttrace
30
+
31
+ Lightweight observability SDK for AI agents. Track runs, steps, tokens, cost, and latency — with **zero blocking overhead**.
32
+
33
+ ```python
34
+ with agenttrace.track_run("my-agent", model="llama-3.3-70b") as run:
35
+ result = llm.call(prompt)
36
+ run.add_step("llm_response", input=prompt, output=result, tokens=150, latency=320)
37
+ ```
38
+
39
+ Completed runs are flushed to your [AgentTrace](https://agenttrace.dev) dashboard in the background — your agent never waits on a network call.
40
+
41
+ ---
42
+
43
+ ## Features
44
+
45
+ - **Async-safe** — per-run objects on the call stack, no global mutable state
46
+ - **Non-blocking** — background worker thread + `queue.Queue`; agent execution is never delayed
47
+ - **Reliable** — exponential backoff retries with jitter (4 attempts by default)
48
+ - **Batching** — configurable batch size and flush interval
49
+ - **Zero dependencies** — stdlib only (`urllib`, `queue`, `threading`, `contextlib`)
50
+ - **Sync + async** — context managers and decorator for both sync and async code
51
+
52
+ ---
53
+
54
+ ## Install
55
+
56
+ ```bash
57
+ pip install agenttrace
58
+ ```
59
+
60
+ ---
61
+
62
+ ## Quick start
63
+
64
+ Set your credentials in `.env` (or export as environment variables):
65
+
66
+ ```env
67
+ AGENTTRACE_API_KEY="at_xxxxxxxxxxxxxxxxxxxx"
68
+ AGENTTRACE_URL="https://your-dashboard.com" # default: http://localhost:3001
69
+ AGENTTRACE_SERVICE="my-agent" # default: agent
70
+ ```
71
+
72
+ ```python
73
+ from dotenv import load_dotenv
74
+ load_dotenv()
75
+
76
+ import agenttrace
77
+
78
+ with agenttrace.track_run("my-agent") as run:
79
+ # ... your agent logic ...
80
+ run.add_step("llm_response", input="Hello", output="Hi there", tokens=20, latency=310)
81
+
82
+ # For scripts: flush before process exit
83
+ agenttrace.flush()
84
+ ```
85
+
86
+ ---
87
+
88
+ ## Usage
89
+
90
+ ### Sync — context manager
91
+
92
+ ```python
93
+ import time
94
+ import agenttrace
95
+
96
+ with agenttrace.track_run(
97
+ "hotel-search-agent",
98
+ model="llama-3.3-70b",
99
+ user_id="user_123",
100
+ tags=["prod", "search"],
101
+ ) as run:
102
+
103
+ # Track an LLM call
104
+ t0 = time.time_ns()
105
+ response = groq_client.chat.completions.create(model=..., messages=...)
106
+ run.add_step(
107
+ "llm_response",
108
+ input=messages[-1]["content"],
109
+ output=response.choices[0].message.content,
110
+ tokens=response.usage.total_tokens,
111
+ latency=(time.time_ns() - t0) // 1_000_000, # ns → ms
112
+ )
113
+
114
+ # Track a tool call
115
+ t1 = time.time_ns()
116
+ results = web_search(query)
117
+ run.add_step(
118
+ "tool_call",
119
+ input=query,
120
+ output=results[:2000],
121
+ latency=(time.time_ns() - t1) // 1_000_000,
122
+ )
123
+ ```
124
+
125
+ The run is enqueued the moment the `with` block exits — whether it succeeded or raised an exception.
126
+
127
+ ### Async — context manager
128
+
129
+ Identical API, works inside `async def` functions and coroutines:
130
+
131
+ ```python
132
+ import agenttrace
133
+
134
+ async def handle_request(query: str) -> str:
135
+ async with agenttrace.async_track_run("my-agent", model="gpt-4o") as run:
136
+ result = await llm.acall(query)
137
+ run.add_step("llm_response", input=query, output=result, tokens=80, latency=500)
138
+ return result
139
+ ```
140
+
141
+ `enqueue()` is synchronous and non-blocking (`queue.put_nowait`), so it is safe to call from async code without `await`.
142
+
143
+ ### Decorator
144
+
145
+ Wrap a function so every call is tracked automatically:
146
+
147
+ ```python
148
+ @agenttrace.traced_run("search-agent") # explicit name
149
+ def search_agent(query: str) -> str:
150
+ ...
151
+
152
+ @agenttrace.traced_run # uses function name
153
+ def code_agent(question: str) -> str:
154
+ ...
155
+ ```
156
+
157
+ The decorator uses `track_run` internally, so exceptions are caught, the run is marked failed, and the error is recorded before re-raising.
158
+
159
+ ### Marking a run failed
160
+
161
+ The context manager catches unhandled exceptions automatically. For explicit failure paths:
162
+
163
+ ```python
164
+ with agenttrace.track_run("my-agent") as run:
165
+ result = call_external_api()
166
+ if result is None:
167
+ run.fail("External API returned no data")
168
+ return
169
+ run.add_step(...)
170
+ ```
171
+
172
+ ---
173
+
174
+ ## Configuration
175
+
176
+ ### Environment variables
177
+
178
+ | Variable | Default | Description |
179
+ |---|---|---|
180
+ | `AGENTTRACE_API_KEY` | — | **Required.** Your API key (`at_...`) |
181
+ | `AGENTTRACE_URL` | `http://localhost:3001` | Backend base URL |
182
+ | `AGENTTRACE_SERVICE` | `agent` | Default service/agent name |
183
+
184
+ ### `agenttrace.init()` — programmatic config
185
+
186
+ Calling `init()` is optional if you use environment variables. Use it to override defaults or tune worker behaviour:
187
+
188
+ ```python
189
+ agenttrace.init(
190
+ api_key="at_xxxxxxxxxxxxxxxxxxxx",
191
+ base_url="https://your-dashboard.com",
192
+ service_name="my-agent",
193
+
194
+ # Worker tuning (optional)
195
+ batch_size=30, # flush after this many runs accumulate (default: 20)
196
+ flush_interval=3.0, # flush every N seconds regardless (default: 2.0)
197
+ max_queue_size=2000, # drop runs with a warning if queue exceeds this (default: 1000)
198
+ max_retries=5, # retry attempts per run on transient errors (default: 4)
199
+ retry_base_delay=1.0, # base backoff in seconds, doubles each retry (default: 0.5)
200
+ )
201
+ ```
202
+
203
+ `init()` can be called multiple times (e.g. in tests) — it replaces the worker and config.
204
+
205
+ ---
206
+
207
+ ## `run.add_step()` reference
208
+
209
+ ```python
210
+ run.add_step(
211
+ step_type, # str — see Step types below
212
+ *,
213
+ input="", # str — prompt / query / tool input
214
+ output="", # str — completion / result / tool output
215
+ tokens=0, # int — total tokens for this step
216
+ latency=0, # int — wall-clock time in milliseconds
217
+ cost=0.0, # float — USD cost for this step
218
+ status="success" # "success" | "failed"
219
+ )
220
+ ```
221
+
222
+ Tokens and cost are **summed automatically** across all steps — you don't need to track totals yourself.
223
+
224
+ ### Step types
225
+
226
+ | `step_type` | When to use |
227
+ |---|---|
228
+ | `"llm_response"` | Any LLM completion call |
229
+ | `"llm_prompt"` | Prompt-only span (before response arrives) |
230
+ | `"tool_call"` | External tool / function call |
231
+ | `"tool_response"` | Response from a tool |
232
+ | `"user_prompt"` | Initial user message |
233
+ | `"decision"` | Routing / branching logic in the agent |
234
+
235
+ ---
236
+
237
+ ## Flushing before exit
238
+
239
+ The background worker sends continuously in long-running processes (servers, workers). For **short-lived scripts**, call `flush()` before the process exits:
240
+
241
+ ```python
242
+ if __name__ == "__main__":
243
+ run_agent(query)
244
+ agenttrace.flush() # waits up to 10s for the queue to drain
245
+ ```
246
+
247
+ An `atexit` handler provides a best-effort flush as a safety net, but an explicit `flush()` is more reliable.
248
+
249
+ ---
250
+
251
+ ## How it works
252
+
253
+ ```
254
+ Agent thread Background worker thread
255
+ ───────────────── ────────────────────────────────
256
+ track_run().__enter__()
257
+ → AgentRun created sleeping (flush_interval elapsed?)
258
+ run.add_step(...)
259
+ → appended to run._steps
260
+ track_run().__exit__()
261
+ → run.to_payload() wakes up: batch_size reached or
262
+ → queue.put_nowait(payload) flush_interval elapsed
263
+ drains up to batch_size items
264
+ POST /agent-metrics (with retry)
265
+ POST /agent-metrics
266
+ ...
267
+ ```
268
+
269
+ The agent thread never waits on the network. If the queue fills up (e.g. backend is down for a long time), new runs are dropped with a warning rather than blocking.
270
+
271
+ ---
272
+
273
+ ## Architecture
274
+
275
+ ```
276
+ agenttrace/
277
+ __init__.py Public API: track_run, async_track_run, traced_run, init, flush
278
+ _config.py Config dataclass — written once at init, read-only thereafter
279
+ _run.py AgentRun — per-invocation context object, lives on the call stack
280
+ _worker.py IngestionWorker — daemon thread, Queue consumer, batching + atexit
281
+ _client.py HTTP POST with exponential backoff retry, stdlib urllib only
282
+ py.typed PEP 561 marker — enables type checking in downstream projects
283
+ ```
284
+
285
+ ---
286
+
287
+ ## Full example
288
+
289
+ ```python
290
+ import json, os, time
291
+ from dotenv import load_dotenv
292
+ load_dotenv()
293
+
294
+ import agenttrace
295
+ from groq import Groq
296
+
297
+ GROQ_MODEL = os.getenv("GROQ_MODEL", "llama-3.3-70b-versatile")
298
+
299
+ def run_agent(query: str) -> str:
300
+ client = Groq()
301
+ messages = [{"role": "user", "content": query}]
302
+
303
+ with agenttrace.track_run("my-agent", model=GROQ_MODEL) as run:
304
+ for _ in range(10):
305
+ t0 = time.time_ns()
306
+ resp = client.chat.completions.create(
307
+ model=GROQ_MODEL, messages=messages, tools=[...], tool_choice="auto"
308
+ )
309
+ run.add_step(
310
+ "llm_response",
311
+ input=messages[-1]["content"],
312
+ output=resp.choices[0].message.content or "",
313
+ tokens=(resp.usage.prompt_tokens + resp.usage.completion_tokens),
314
+ latency=(time.time_ns() - t0) // 1_000_000,
315
+ )
316
+
317
+ msg = resp.choices[0].message
318
+ if not msg.tool_calls:
319
+ return msg.content
320
+
321
+ for tc in msg.tool_calls:
322
+ args = json.loads(tc.function.arguments)
323
+ t1 = time.time_ns()
324
+ result = my_tool(**args)
325
+ run.add_step(
326
+ "tool_call",
327
+ input=json.dumps(args),
328
+ output=str(result)[:2000],
329
+ latency=(time.time_ns() - t1) // 1_000_000,
330
+ )
331
+ messages.append({"role": "tool", "tool_call_id": tc.id, "content": result})
332
+
333
+ run.fail("Max iterations reached")
334
+ return ""
335
+
336
+ if __name__ == "__main__":
337
+ print(run_agent("best hotels in Bangalore"))
338
+ agenttrace.flush()
339
+ ```
340
+
341
+ ---
342
+
343
+ ## License
344
+
345
+ MIT
@@ -0,0 +1,317 @@
1
+ # agenttrace
2
+
3
+ Lightweight observability SDK for AI agents. Track runs, steps, tokens, cost, and latency — with **zero blocking overhead**.
4
+
5
+ ```python
6
+ with agenttrace.track_run("my-agent", model="llama-3.3-70b") as run:
7
+ result = llm.call(prompt)
8
+ run.add_step("llm_response", input=prompt, output=result, tokens=150, latency=320)
9
+ ```
10
+
11
+ Completed runs are flushed to your [AgentTrace](https://agenttrace.dev) dashboard in the background — your agent never waits on a network call.
12
+
13
+ ---
14
+
15
+ ## Features
16
+
17
+ - **Async-safe** — per-run objects on the call stack, no global mutable state
18
+ - **Non-blocking** — background worker thread + `queue.Queue`; agent execution is never delayed
19
+ - **Reliable** — exponential backoff retries with jitter (4 attempts by default)
20
+ - **Batching** — configurable batch size and flush interval
21
+ - **Zero dependencies** — stdlib only (`urllib`, `queue`, `threading`, `contextlib`)
22
+ - **Sync + async** — context managers and decorator for both sync and async code
23
+
24
+ ---
25
+
26
+ ## Install
27
+
28
+ ```bash
29
+ pip install agenttrace
30
+ ```
31
+
32
+ ---
33
+
34
+ ## Quick start
35
+
36
+ Set your credentials in `.env` (or export as environment variables):
37
+
38
+ ```env
39
+ AGENTTRACE_API_KEY="at_xxxxxxxxxxxxxxxxxxxx"
40
+ AGENTTRACE_URL="https://your-dashboard.com" # default: http://localhost:3001
41
+ AGENTTRACE_SERVICE="my-agent" # default: agent
42
+ ```
43
+
44
+ ```python
45
+ from dotenv import load_dotenv
46
+ load_dotenv()
47
+
48
+ import agenttrace
49
+
50
+ with agenttrace.track_run("my-agent") as run:
51
+ # ... your agent logic ...
52
+ run.add_step("llm_response", input="Hello", output="Hi there", tokens=20, latency=310)
53
+
54
+ # For scripts: flush before process exit
55
+ agenttrace.flush()
56
+ ```
57
+
58
+ ---
59
+
60
+ ## Usage
61
+
62
+ ### Sync — context manager
63
+
64
+ ```python
65
+ import time
66
+ import agenttrace
67
+
68
+ with agenttrace.track_run(
69
+ "hotel-search-agent",
70
+ model="llama-3.3-70b",
71
+ user_id="user_123",
72
+ tags=["prod", "search"],
73
+ ) as run:
74
+
75
+ # Track an LLM call
76
+ t0 = time.time_ns()
77
+ response = groq_client.chat.completions.create(model=..., messages=...)
78
+ run.add_step(
79
+ "llm_response",
80
+ input=messages[-1]["content"],
81
+ output=response.choices[0].message.content,
82
+ tokens=response.usage.total_tokens,
83
+ latency=(time.time_ns() - t0) // 1_000_000, # ns → ms
84
+ )
85
+
86
+ # Track a tool call
87
+ t1 = time.time_ns()
88
+ results = web_search(query)
89
+ run.add_step(
90
+ "tool_call",
91
+ input=query,
92
+ output=results[:2000],
93
+ latency=(time.time_ns() - t1) // 1_000_000,
94
+ )
95
+ ```
96
+
97
+ The run is enqueued the moment the `with` block exits — whether it succeeded or raised an exception.
98
+
99
+ ### Async — context manager
100
+
101
+ Identical API, works inside `async def` functions and coroutines:
102
+
103
+ ```python
104
+ import agenttrace
105
+
106
+ async def handle_request(query: str) -> str:
107
+ async with agenttrace.async_track_run("my-agent", model="gpt-4o") as run:
108
+ result = await llm.acall(query)
109
+ run.add_step("llm_response", input=query, output=result, tokens=80, latency=500)
110
+ return result
111
+ ```
112
+
113
+ `enqueue()` is synchronous and non-blocking (`queue.put_nowait`), so it is safe to call from async code without `await`.
114
+
115
+ ### Decorator
116
+
117
+ Wrap a function so every call is tracked automatically:
118
+
119
+ ```python
120
+ @agenttrace.traced_run("search-agent") # explicit name
121
+ def search_agent(query: str) -> str:
122
+ ...
123
+
124
+ @agenttrace.traced_run # uses function name
125
+ def code_agent(question: str) -> str:
126
+ ...
127
+ ```
128
+
129
+ The decorator uses `track_run` internally, so exceptions are caught, the run is marked failed, and the error is recorded before re-raising.
130
+
131
+ ### Marking a run failed
132
+
133
+ The context manager catches unhandled exceptions automatically. For explicit failure paths:
134
+
135
+ ```python
136
+ with agenttrace.track_run("my-agent") as run:
137
+ result = call_external_api()
138
+ if result is None:
139
+ run.fail("External API returned no data")
140
+ return
141
+ run.add_step(...)
142
+ ```
143
+
144
+ ---
145
+
146
+ ## Configuration
147
+
148
+ ### Environment variables
149
+
150
+ | Variable | Default | Description |
151
+ |---|---|---|
152
+ | `AGENTTRACE_API_KEY` | — | **Required.** Your API key (`at_...`) |
153
+ | `AGENTTRACE_URL` | `http://localhost:3001` | Backend base URL |
154
+ | `AGENTTRACE_SERVICE` | `agent` | Default service/agent name |
155
+
156
+ ### `agenttrace.init()` — programmatic config
157
+
158
+ Calling `init()` is optional if you use environment variables. Use it to override defaults or tune worker behaviour:
159
+
160
+ ```python
161
+ agenttrace.init(
162
+ api_key="at_xxxxxxxxxxxxxxxxxxxx",
163
+ base_url="https://your-dashboard.com",
164
+ service_name="my-agent",
165
+
166
+ # Worker tuning (optional)
167
+ batch_size=30, # flush after this many runs accumulate (default: 20)
168
+ flush_interval=3.0, # flush every N seconds regardless (default: 2.0)
169
+ max_queue_size=2000, # drop runs with a warning if queue exceeds this (default: 1000)
170
+ max_retries=5, # retry attempts per run on transient errors (default: 4)
171
+ retry_base_delay=1.0, # base backoff in seconds, doubles each retry (default: 0.5)
172
+ )
173
+ ```
174
+
175
+ `init()` can be called multiple times (e.g. in tests) — it replaces the worker and config.
176
+
177
+ ---
178
+
179
+ ## `run.add_step()` reference
180
+
181
+ ```python
182
+ run.add_step(
183
+ step_type, # str — see Step types below
184
+ *,
185
+ input="", # str — prompt / query / tool input
186
+ output="", # str — completion / result / tool output
187
+ tokens=0, # int — total tokens for this step
188
+ latency=0, # int — wall-clock time in milliseconds
189
+ cost=0.0, # float — USD cost for this step
190
+ status="success" # "success" | "failed"
191
+ )
192
+ ```
193
+
194
+ Tokens and cost are **summed automatically** across all steps — you don't need to track totals yourself.
195
+
196
+ ### Step types
197
+
198
+ | `step_type` | When to use |
199
+ |---|---|
200
+ | `"llm_response"` | Any LLM completion call |
201
+ | `"llm_prompt"` | Prompt-only span (before response arrives) |
202
+ | `"tool_call"` | External tool / function call |
203
+ | `"tool_response"` | Response from a tool |
204
+ | `"user_prompt"` | Initial user message |
205
+ | `"decision"` | Routing / branching logic in the agent |
206
+
207
+ ---
208
+
209
+ ## Flushing before exit
210
+
211
+ The background worker sends continuously in long-running processes (servers, workers). For **short-lived scripts**, call `flush()` before the process exits:
212
+
213
+ ```python
214
+ if __name__ == "__main__":
215
+ run_agent(query)
216
+ agenttrace.flush() # waits up to 10s for the queue to drain
217
+ ```
218
+
219
+ An `atexit` handler provides a best-effort flush as a safety net, but an explicit `flush()` is more reliable.
220
+
221
+ ---
222
+
223
+ ## How it works
224
+
225
+ ```
226
+ Agent thread Background worker thread
227
+ ───────────────── ────────────────────────────────
228
+ track_run().__enter__()
229
+ → AgentRun created sleeping (flush_interval elapsed?)
230
+ run.add_step(...)
231
+ → appended to run._steps
232
+ track_run().__exit__()
233
+ → run.to_payload() wakes up: batch_size reached or
234
+ → queue.put_nowait(payload) flush_interval elapsed
235
+ drains up to batch_size items
236
+ POST /agent-metrics (with retry)
237
+ POST /agent-metrics
238
+ ...
239
+ ```
240
+
241
+ The agent thread never waits on the network. If the queue fills up (e.g. backend is down for a long time), new runs are dropped with a warning rather than blocking.
242
+
243
+ ---
244
+
245
+ ## Architecture
246
+
247
+ ```
248
+ agenttrace/
249
+ __init__.py Public API: track_run, async_track_run, traced_run, init, flush
250
+ _config.py Config dataclass — written once at init, read-only thereafter
251
+ _run.py AgentRun — per-invocation context object, lives on the call stack
252
+ _worker.py IngestionWorker — daemon thread, Queue consumer, batching + atexit
253
+ _client.py HTTP POST with exponential backoff retry, stdlib urllib only
254
+ py.typed PEP 561 marker — enables type checking in downstream projects
255
+ ```
256
+
257
+ ---
258
+
259
+ ## Full example
260
+
261
+ ```python
262
+ import json, os, time
263
+ from dotenv import load_dotenv
264
+ load_dotenv()
265
+
266
+ import agenttrace
267
+ from groq import Groq
268
+
269
+ GROQ_MODEL = os.getenv("GROQ_MODEL", "llama-3.3-70b-versatile")
270
+
271
+ def run_agent(query: str) -> str:
272
+ client = Groq()
273
+ messages = [{"role": "user", "content": query}]
274
+
275
+ with agenttrace.track_run("my-agent", model=GROQ_MODEL) as run:
276
+ for _ in range(10):
277
+ t0 = time.time_ns()
278
+ resp = client.chat.completions.create(
279
+ model=GROQ_MODEL, messages=messages, tools=[...], tool_choice="auto"
280
+ )
281
+ run.add_step(
282
+ "llm_response",
283
+ input=messages[-1]["content"],
284
+ output=resp.choices[0].message.content or "",
285
+ tokens=(resp.usage.prompt_tokens + resp.usage.completion_tokens),
286
+ latency=(time.time_ns() - t0) // 1_000_000,
287
+ )
288
+
289
+ msg = resp.choices[0].message
290
+ if not msg.tool_calls:
291
+ return msg.content
292
+
293
+ for tc in msg.tool_calls:
294
+ args = json.loads(tc.function.arguments)
295
+ t1 = time.time_ns()
296
+ result = my_tool(**args)
297
+ run.add_step(
298
+ "tool_call",
299
+ input=json.dumps(args),
300
+ output=str(result)[:2000],
301
+ latency=(time.time_ns() - t1) // 1_000_000,
302
+ )
303
+ messages.append({"role": "tool", "tool_call_id": tc.id, "content": result})
304
+
305
+ run.fail("Max iterations reached")
306
+ return ""
307
+
308
+ if __name__ == "__main__":
309
+ print(run_agent("best hotels in Bangalore"))
310
+ agenttrace.flush()
311
+ ```
312
+
313
+ ---
314
+
315
+ ## License
316
+
317
+ MIT