agentpulse-py 0.0.1__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.
- agentpulse_py-0.0.1/.gitignore +8 -0
- agentpulse_py-0.0.1/PKG-INFO +345 -0
- agentpulse_py-0.0.1/README.md +317 -0
- agentpulse_py-0.0.1/agentpulse/__init__.py +264 -0
- agentpulse_py-0.0.1/agentpulse/_client.py +102 -0
- agentpulse_py-0.0.1/agentpulse/_config.py +34 -0
- agentpulse_py-0.0.1/agentpulse/_run.py +152 -0
- agentpulse_py-0.0.1/agentpulse/_send.py +87 -0
- agentpulse_py-0.0.1/agentpulse/_worker.py +152 -0
- agentpulse_py-0.0.1/agentpulse/_wrap_anthropic.py +227 -0
- agentpulse_py-0.0.1/agentpulse/_wrap_bedrock.py +152 -0
- agentpulse_py-0.0.1/agentpulse/_wrap_gemini.py +144 -0
- agentpulse_py-0.0.1/agentpulse/_wrap_grok.py +23 -0
- agentpulse_py-0.0.1/agentpulse/_wrap_openai.py +159 -0
- agentpulse_py-0.0.1/agentpulse/py.typed +0 -0
- agentpulse_py-0.0.1/pyproject.toml +39 -0
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: agentpulse-py
|
|
3
|
+
Version: 0.0.1
|
|
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
|