useagentledger 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,141 @@
1
+ Metadata-Version: 2.4
2
+ Name: useagentledger
3
+ Version: 0.1.0
4
+ Summary: AgentLedger Python SDK — track, attribute, and govern AI agent spend across OpenAI, Anthropic, and Gemini.
5
+ License: MIT
6
+ Project-URL: Homepage, https://useagentledger.com
7
+ Project-URL: Documentation, https://useagentledger.com/blog
8
+ Keywords: llm,cost-tracking,ai-agents,openai,anthropic,gemini,observability
9
+ Classifier: Development Status :: 4 - Beta
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Topic :: Software Development :: Libraries
13
+ Requires-Python: >=3.9
14
+ Description-Content-Type: text/markdown
15
+
16
+ # AgentLedger Python SDK
17
+
18
+ Track, attribute, and govern AI agent spend across OpenAI, Anthropic, and Gemini — with per-agent / per-task / per-user / per-customer cost attribution, kill switches, loop detection, and ROI reporting.
19
+
20
+ Zero dependencies. Python 3.9+.
21
+
22
+ ```bash
23
+ pip install useagentledger
24
+ ```
25
+
26
+ (The import name is `agentledger`: `from agentledger import AgentLedger`.)
27
+
28
+ ## Quick start
29
+
30
+ ```python
31
+ from agentledger import AgentLedger
32
+
33
+ al = AgentLedger(api_key="al_live_...") # provider keys read from env by default
34
+
35
+ response = al.run(
36
+ agent="invoice-processor",
37
+ task="extraction",
38
+ customer="acme-corp",
39
+ llm={
40
+ "provider": "anthropic",
41
+ "model": "claude-sonnet-4-6",
42
+ "max_tokens": 1024,
43
+ "messages": [{"role": "user", "content": "Extract line items from this invoice: ..."}],
44
+ },
45
+ )
46
+ print(response["content"][0]["text"])
47
+ ```
48
+
49
+ Every call is tracked automatically — token counts, latency, and attribution dimensions. Cost is computed server-side from AgentLedger's pricing table (134+ models), so the SDK never goes stale.
50
+
51
+ ## Kill switch
52
+
53
+ `run()` checks AgentLedger's block list before every call (cached 5 seconds). If an alert rule has blocked this agent, task, user, or customer, the call raises **before** any provider spend happens:
54
+
55
+ ```python
56
+ from agentledger import AgentLedger, AgentLedgerBlockError
57
+
58
+ try:
59
+ al.run(agent="support-bot", customer="acme-corp", llm={...})
60
+ except AgentLedgerBlockError as e:
61
+ print(f"Blocked: {e} (dimension={e.dimension}, value={e.dimension_value})")
62
+ ```
63
+
64
+ The check **fails open** — if AgentLedger is unreachable, your LLM call proceeds normally. Governance never becomes a point of failure.
65
+
66
+ ## Streaming
67
+
68
+ Pass `stream=True` and iterate the returned generator. Usage is captured from the stream's own events and tracked when the stream ends (even if you stop early):
69
+
70
+ ```python
71
+ stream = al.run(
72
+ agent="chat-assistant",
73
+ user="user-42",
74
+ llm={
75
+ "provider": "openai",
76
+ "model": "gpt-4o",
77
+ "stream": True,
78
+ "messages": [{"role": "user", "content": "Hello!"}],
79
+ },
80
+ )
81
+ for event in stream:
82
+ delta = event.get("choices", [{}])[0].get("delta", {}).get("content")
83
+ if delta:
84
+ print(delta, end="", flush=True)
85
+ ```
86
+
87
+ Works for all three providers (Anthropic `message_start`/`message_delta` events, OpenAI usage chunk, Gemini `usageMetadata`).
88
+
89
+ ## Sessions (multi-step agents)
90
+
91
+ Group a multi-step task into one session so loop detection and per-session cost work across steps:
92
+
93
+ ```python
94
+ session_id = al.start_session(agent="research-bot", task="report")
95
+ for step in plan:
96
+ al.run(agent="research-bot", session_id=session_id, llm={...})
97
+ al.end_session(session_id, success=True)
98
+ ```
99
+
100
+ Calls without an explicit session are auto-grouped server-side (same agent + task within a 10-minute window).
101
+
102
+ ## Track calls you make yourself
103
+
104
+ Already using the official `openai` / `anthropic` clients? Just report usage:
105
+
106
+ ```python
107
+ result = openai_client.chat.completions.create(model="gpt-4o-mini", messages=msgs)
108
+
109
+ al.track(
110
+ agent="summarizer",
111
+ customer="acme-corp",
112
+ provider="openai",
113
+ model="gpt-4o-mini",
114
+ input_tokens=result.usage.prompt_tokens,
115
+ output_tokens=result.usage.completion_tokens,
116
+ )
117
+ ```
118
+
119
+ `track()` runs on a background thread by default (zero added latency) and swallows all errors. Pass `background=False` to send synchronously.
120
+
121
+ ## ROI units
122
+
123
+ Record completed business outcomes for cost-per-outcome reporting (`custom_event` count method):
124
+
125
+ ```python
126
+ al.record_unit(agent="invoice-processor", unit_count=1)
127
+ ```
128
+
129
+ ## Configuration
130
+
131
+ ```python
132
+ al = AgentLedger(
133
+ api_key="al_live_...",
134
+ base_url="https://api.useagentledger.com", # or your self-hosted proxy
135
+ provider_keys={ # optional — env vars used otherwise
136
+ "anthropic": "...", # ANTHROPIC_API_KEY
137
+ "openai": "...", # OPENAI_API_KEY
138
+ "gemini": "...", # GOOGLE_API_KEY
139
+ },
140
+ )
141
+ ```
@@ -0,0 +1,126 @@
1
+ # AgentLedger Python SDK
2
+
3
+ Track, attribute, and govern AI agent spend across OpenAI, Anthropic, and Gemini — with per-agent / per-task / per-user / per-customer cost attribution, kill switches, loop detection, and ROI reporting.
4
+
5
+ Zero dependencies. Python 3.9+.
6
+
7
+ ```bash
8
+ pip install useagentledger
9
+ ```
10
+
11
+ (The import name is `agentledger`: `from agentledger import AgentLedger`.)
12
+
13
+ ## Quick start
14
+
15
+ ```python
16
+ from agentledger import AgentLedger
17
+
18
+ al = AgentLedger(api_key="al_live_...") # provider keys read from env by default
19
+
20
+ response = al.run(
21
+ agent="invoice-processor",
22
+ task="extraction",
23
+ customer="acme-corp",
24
+ llm={
25
+ "provider": "anthropic",
26
+ "model": "claude-sonnet-4-6",
27
+ "max_tokens": 1024,
28
+ "messages": [{"role": "user", "content": "Extract line items from this invoice: ..."}],
29
+ },
30
+ )
31
+ print(response["content"][0]["text"])
32
+ ```
33
+
34
+ Every call is tracked automatically — token counts, latency, and attribution dimensions. Cost is computed server-side from AgentLedger's pricing table (134+ models), so the SDK never goes stale.
35
+
36
+ ## Kill switch
37
+
38
+ `run()` checks AgentLedger's block list before every call (cached 5 seconds). If an alert rule has blocked this agent, task, user, or customer, the call raises **before** any provider spend happens:
39
+
40
+ ```python
41
+ from agentledger import AgentLedger, AgentLedgerBlockError
42
+
43
+ try:
44
+ al.run(agent="support-bot", customer="acme-corp", llm={...})
45
+ except AgentLedgerBlockError as e:
46
+ print(f"Blocked: {e} (dimension={e.dimension}, value={e.dimension_value})")
47
+ ```
48
+
49
+ The check **fails open** — if AgentLedger is unreachable, your LLM call proceeds normally. Governance never becomes a point of failure.
50
+
51
+ ## Streaming
52
+
53
+ Pass `stream=True` and iterate the returned generator. Usage is captured from the stream's own events and tracked when the stream ends (even if you stop early):
54
+
55
+ ```python
56
+ stream = al.run(
57
+ agent="chat-assistant",
58
+ user="user-42",
59
+ llm={
60
+ "provider": "openai",
61
+ "model": "gpt-4o",
62
+ "stream": True,
63
+ "messages": [{"role": "user", "content": "Hello!"}],
64
+ },
65
+ )
66
+ for event in stream:
67
+ delta = event.get("choices", [{}])[0].get("delta", {}).get("content")
68
+ if delta:
69
+ print(delta, end="", flush=True)
70
+ ```
71
+
72
+ Works for all three providers (Anthropic `message_start`/`message_delta` events, OpenAI usage chunk, Gemini `usageMetadata`).
73
+
74
+ ## Sessions (multi-step agents)
75
+
76
+ Group a multi-step task into one session so loop detection and per-session cost work across steps:
77
+
78
+ ```python
79
+ session_id = al.start_session(agent="research-bot", task="report")
80
+ for step in plan:
81
+ al.run(agent="research-bot", session_id=session_id, llm={...})
82
+ al.end_session(session_id, success=True)
83
+ ```
84
+
85
+ Calls without an explicit session are auto-grouped server-side (same agent + task within a 10-minute window).
86
+
87
+ ## Track calls you make yourself
88
+
89
+ Already using the official `openai` / `anthropic` clients? Just report usage:
90
+
91
+ ```python
92
+ result = openai_client.chat.completions.create(model="gpt-4o-mini", messages=msgs)
93
+
94
+ al.track(
95
+ agent="summarizer",
96
+ customer="acme-corp",
97
+ provider="openai",
98
+ model="gpt-4o-mini",
99
+ input_tokens=result.usage.prompt_tokens,
100
+ output_tokens=result.usage.completion_tokens,
101
+ )
102
+ ```
103
+
104
+ `track()` runs on a background thread by default (zero added latency) and swallows all errors. Pass `background=False` to send synchronously.
105
+
106
+ ## ROI units
107
+
108
+ Record completed business outcomes for cost-per-outcome reporting (`custom_event` count method):
109
+
110
+ ```python
111
+ al.record_unit(agent="invoice-processor", unit_count=1)
112
+ ```
113
+
114
+ ## Configuration
115
+
116
+ ```python
117
+ al = AgentLedger(
118
+ api_key="al_live_...",
119
+ base_url="https://api.useagentledger.com", # or your self-hosted proxy
120
+ provider_keys={ # optional — env vars used otherwise
121
+ "anthropic": "...", # ANTHROPIC_API_KEY
122
+ "openai": "...", # OPENAI_API_KEY
123
+ "gemini": "...", # GOOGLE_API_KEY
124
+ },
125
+ )
126
+ ```
@@ -0,0 +1,418 @@
1
+ """AgentLedger Python SDK.
2
+
3
+ Track, attribute, and govern AI agent spend across OpenAI, Anthropic, and
4
+ Gemini. Mirrors the TypeScript SDK:
5
+
6
+ - ``run()`` wraps an LLM call with a pre-call kill-switch check and
7
+ fire-and-forget usage tracking (cost is computed server-side from
8
+ AgentLedger's authoritative pricing table — the SDK carries no pricing copy).
9
+ - Control-plane calls (block checks, tracking, sessions) always fail open:
10
+ a network error on AgentLedger's side never breaks your LLM call.
11
+
12
+ Zero dependencies — stdlib only.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import hashlib
18
+ import json
19
+ import threading
20
+ import time
21
+ import urllib.error
22
+ import urllib.request
23
+ from typing import Any, Dict, Generator, Iterable, List, Optional
24
+
25
+ __all__ = ["AgentLedger", "AgentLedgerBlockError"]
26
+
27
+ _BLOCK_CACHE_TTL = 5.0 # seconds, matches the TS SDK and proxy cache
28
+ _CONTROL_PLANE_TIMEOUT = 10.0
29
+ _LLM_TIMEOUT = 600.0
30
+
31
+ _ENV_VARS = {
32
+ "anthropic": "ANTHROPIC_API_KEY",
33
+ "openai": "OPENAI_API_KEY",
34
+ "gemini": "GOOGLE_API_KEY",
35
+ }
36
+
37
+
38
+ class AgentLedgerBlockError(Exception):
39
+ """Raised when the AgentLedger kill switch blocks a call before it is made."""
40
+
41
+ def __init__(self, message: str, dimension: str, dimension_value: str):
42
+ super().__init__(message)
43
+ self.dimension = dimension
44
+ self.dimension_value = dimension_value
45
+
46
+
47
+ def _post_json(url: str, body: Dict[str, Any], headers: Dict[str, str], timeout: float) -> Dict[str, Any]:
48
+ data = json.dumps(body).encode("utf-8")
49
+ req = urllib.request.Request(url, data=data, method="POST")
50
+ req.add_header("Content-Type", "application/json")
51
+ for k, v in headers.items():
52
+ req.add_header(k, v)
53
+ with urllib.request.urlopen(req, timeout=timeout) as res:
54
+ return json.loads(res.read().decode("utf-8"))
55
+
56
+
57
+ def _iter_sse(res: Any) -> Generator[Dict[str, Any], None, None]:
58
+ """Parse a text/event-stream response into JSON event dicts."""
59
+ for raw_line in res:
60
+ line = raw_line.decode("utf-8").rstrip("\r\n")
61
+ if not line.startswith("data:"):
62
+ continue
63
+ payload = line[5:].strip()
64
+ if payload == "[DONE]":
65
+ return
66
+ try:
67
+ yield json.loads(payload)
68
+ except (ValueError, TypeError):
69
+ continue # skip malformed event data
70
+
71
+
72
+ class AgentLedger:
73
+ """Client for tracking and governing LLM calls through AgentLedger.
74
+
75
+ Args:
76
+ api_key: Your AgentLedger API key (``al_live_...``).
77
+ base_url: AgentLedger API base URL. Defaults to the hosted service.
78
+ provider_keys: Optional dict with ``anthropic`` / ``openai`` / ``gemini``
79
+ API keys. Falls back to ``ANTHROPIC_API_KEY`` / ``OPENAI_API_KEY`` /
80
+ ``GOOGLE_API_KEY`` environment variables.
81
+ """
82
+
83
+ def __init__(
84
+ self,
85
+ api_key: str,
86
+ base_url: str = "https://api.useagentledger.com",
87
+ provider_keys: Optional[Dict[str, str]] = None,
88
+ ):
89
+ import os
90
+
91
+ self._api_key = api_key
92
+ self._base_url = base_url.rstrip("/")
93
+ keys = provider_keys or {}
94
+ self._provider_keys = {
95
+ provider: keys.get(provider) or os.environ.get(env_var, "")
96
+ for provider, env_var in _ENV_VARS.items()
97
+ }
98
+ self._block_cache: Dict[str, Dict[str, Any]] = {}
99
+ self._block_cache_lock = threading.Lock()
100
+
101
+ # ── Control plane ─────────────────────────────────────────────────────────
102
+
103
+ def _auth_headers(self) -> Dict[str, str]:
104
+ return {"Authorization": f"Bearer {self._api_key}"}
105
+
106
+ def _check_blocks(
107
+ self,
108
+ agent: str,
109
+ task: Optional[str],
110
+ user: Optional[str],
111
+ customer: Optional[str],
112
+ ) -> None:
113
+ cache_key = f"{agent}:{task or ''}:{user or ''}:{customer or ''}"
114
+ now = time.monotonic()
115
+
116
+ with self._block_cache_lock:
117
+ cached = self._block_cache.get(cache_key)
118
+ if cached and now < cached["expires_at"]:
119
+ if cached["isBlocked"]:
120
+ raise AgentLedgerBlockError(
121
+ f"AgentLedger kill switch: {cached['reason']}",
122
+ cached["dimension"],
123
+ cached["dimension_value"],
124
+ )
125
+ return
126
+ if cached:
127
+ del self._block_cache[cache_key]
128
+
129
+ try:
130
+ status = _post_json(
131
+ f"{self._base_url}/api/v1/blocks/check",
132
+ {"agent": agent, "task": task, "user_id": user, "customer_id": customer},
133
+ self._auth_headers(),
134
+ _CONTROL_PLANE_TIMEOUT,
135
+ )
136
+ except Exception:
137
+ return # fail open — AgentLedger being unreachable never blocks the call
138
+
139
+ with self._block_cache_lock:
140
+ self._block_cache[cache_key] = {**status, "expires_at": now + _BLOCK_CACHE_TTL}
141
+
142
+ if status.get("isBlocked"):
143
+ raise AgentLedgerBlockError(
144
+ f"AgentLedger kill switch: {status.get('reason', '')}",
145
+ status.get("dimension", ""),
146
+ status.get("dimension_value", ""),
147
+ )
148
+
149
+ def track(
150
+ self,
151
+ agent: str,
152
+ provider: str,
153
+ model: str,
154
+ input_tokens: int,
155
+ output_tokens: int,
156
+ task: Optional[str] = None,
157
+ user_id: Optional[str] = None,
158
+ customer_id: Optional[str] = None,
159
+ cost_usd: Optional[float] = None,
160
+ latency_ms: Optional[int] = None,
161
+ metadata: Optional[Dict[str, Any]] = None,
162
+ session_id: Optional[str] = None,
163
+ input_hash: Optional[str] = None,
164
+ background: bool = True,
165
+ ) -> None:
166
+ """Record an LLM call. Cost is computed server-side when ``cost_usd`` is omitted.
167
+
168
+ With ``background=True`` (default) the HTTP call runs on a daemon thread
169
+ so tracking adds no latency to your agent. Errors are always swallowed.
170
+ """
171
+ body = {
172
+ "agent": agent,
173
+ "task": task,
174
+ "user_id": user_id,
175
+ "customer_id": customer_id,
176
+ "provider": provider,
177
+ "model": model,
178
+ "input_tokens": input_tokens,
179
+ "output_tokens": output_tokens,
180
+ "session_id": session_id,
181
+ "input_hash": input_hash,
182
+ }
183
+ if cost_usd is not None:
184
+ body["cost_usd"] = cost_usd
185
+ if latency_ms is not None:
186
+ body["latency_ms"] = latency_ms
187
+ if metadata is not None:
188
+ body["metadata"] = metadata
189
+
190
+ def send() -> None:
191
+ try:
192
+ _post_json(f"{self._base_url}/api/v1/track", body, self._auth_headers(), _CONTROL_PLANE_TIMEOUT)
193
+ except Exception:
194
+ pass # tracking failures never surface
195
+
196
+ if background:
197
+ threading.Thread(target=send, daemon=True).start()
198
+ else:
199
+ send()
200
+
201
+ def start_session(
202
+ self,
203
+ agent: str,
204
+ task: Optional[str] = None,
205
+ user: Optional[str] = None,
206
+ customer: Optional[str] = None,
207
+ ) -> Optional[str]:
208
+ """Create an explicit session for grouping multi-step agent work. Returns the session ID, or None on failure."""
209
+ try:
210
+ res = _post_json(
211
+ f"{self._base_url}/api/v1/track/sessions",
212
+ {"agent": agent, "task": task, "user_id": user, "customer_id": customer},
213
+ self._auth_headers(),
214
+ _CONTROL_PLANE_TIMEOUT,
215
+ )
216
+ return res.get("session_id")
217
+ except Exception:
218
+ return None
219
+
220
+ def end_session(self, session_id: str, success: bool = True) -> None:
221
+ try:
222
+ data = json.dumps({"success": success}).encode("utf-8")
223
+ req = urllib.request.Request(
224
+ f"{self._base_url}/api/v1/track/sessions/{session_id}", data=data, method="PATCH"
225
+ )
226
+ req.add_header("Content-Type", "application/json")
227
+ req.add_header("Authorization", f"Bearer {self._api_key}")
228
+ urllib.request.urlopen(req, timeout=_CONTROL_PLANE_TIMEOUT).close()
229
+ except Exception:
230
+ pass # session errors never surface
231
+
232
+ def record_unit(
233
+ self,
234
+ agent: str,
235
+ session_id: Optional[str] = None,
236
+ unit_count: int = 1,
237
+ metadata: Optional[Dict[str, Any]] = None,
238
+ ) -> None:
239
+ """Record a completed business unit (for ROI reporting with custom_event counting)."""
240
+ try:
241
+ _post_json(
242
+ f"{self._base_url}/api/v1/roi/events",
243
+ {"agent": agent, "session_id": session_id, "unit_count": unit_count, "metadata": metadata},
244
+ self._auth_headers(),
245
+ _CONTROL_PLANE_TIMEOUT,
246
+ )
247
+ except Exception:
248
+ pass # never surface recording errors
249
+
250
+ # ── LLM execution ─────────────────────────────────────────────────────────
251
+
252
+ @staticmethod
253
+ def _hash_messages(messages: Iterable[Any]) -> Optional[str]:
254
+ try:
255
+ s = json.dumps(list(messages), separators=(",", ":"), default=str)[:4000]
256
+ return hashlib.sha256(s.encode("utf-8")).hexdigest()[:16]
257
+ except Exception:
258
+ return None
259
+
260
+ def _require_key(self, provider: str) -> str:
261
+ key = self._provider_keys.get(provider, "")
262
+ if not key:
263
+ raise ValueError(
264
+ f"{provider} API key not set. Pass provider_keys={{'{provider}': ...}} or set {_ENV_VARS[provider]}."
265
+ )
266
+ return key
267
+
268
+ def _provider_request(
269
+ self, provider: str, model: str, messages: List[Any], options: Dict[str, Any], stream: bool
270
+ ) -> urllib.request.Request:
271
+ if provider == "anthropic":
272
+ key = self._require_key("anthropic")
273
+ body = {"model": model, "messages": messages, **options}
274
+ if stream:
275
+ body["stream"] = True
276
+ req = urllib.request.Request(
277
+ "https://api.anthropic.com/v1/messages",
278
+ data=json.dumps(body).encode("utf-8"),
279
+ method="POST",
280
+ )
281
+ req.add_header("x-api-key", key)
282
+ req.add_header("anthropic-version", "2023-06-01")
283
+ elif provider == "openai":
284
+ key = self._require_key("openai")
285
+ body = {"model": model, "messages": messages, **options}
286
+ if stream:
287
+ body["stream"] = True
288
+ # include_usage makes OpenAI emit a final chunk carrying token usage
289
+ body.setdefault("stream_options", {"include_usage": True})
290
+ req = urllib.request.Request(
291
+ "https://api.openai.com/v1/chat/completions",
292
+ data=json.dumps(body).encode("utf-8"),
293
+ method="POST",
294
+ )
295
+ req.add_header("Authorization", f"Bearer {key}")
296
+ elif provider == "gemini":
297
+ key = self._require_key("gemini")
298
+ action = "streamGenerateContent?alt=sse" if stream else "generateContent"
299
+ body = {"contents": messages, **options}
300
+ req = urllib.request.Request(
301
+ f"https://generativelanguage.googleapis.com/v1beta/models/{model}:{action}",
302
+ data=json.dumps(body).encode("utf-8"),
303
+ method="POST",
304
+ )
305
+ req.add_header("x-goog-api-key", key)
306
+ else:
307
+ raise ValueError(f"Unsupported provider: {provider}. Use 'anthropic', 'openai', or 'gemini'.")
308
+
309
+ req.add_header("Content-Type", "application/json")
310
+ return req
311
+
312
+ def run(
313
+ self,
314
+ agent: str,
315
+ llm: Dict[str, Any],
316
+ task: Optional[str] = None,
317
+ user: Optional[str] = None,
318
+ customer: Optional[str] = None,
319
+ session_id: Optional[str] = None,
320
+ ) -> Any:
321
+ """Run an LLM call with kill-switch enforcement and automatic tracking.
322
+
323
+ ``llm`` must contain ``provider`` ('anthropic' | 'openai' | 'gemini'),
324
+ ``model``, and ``messages``; any other keys are passed through to the
325
+ provider (e.g. ``max_tokens``, ``temperature``).
326
+
327
+ Non-streaming (default): returns the provider's JSON response as a dict.
328
+ Streaming (``stream=True`` in ``llm``): returns a generator of parsed
329
+ stream events. Usage is tracked when the stream ends, including early
330
+ termination by the consumer.
331
+
332
+ Raises:
333
+ AgentLedgerBlockError: if an active kill-switch block matches this call.
334
+ """
335
+ llm = dict(llm)
336
+ provider = llm.pop("provider")
337
+ model = llm.pop("model")
338
+ messages = llm.pop("messages")
339
+ stream = bool(llm.pop("stream", False))
340
+
341
+ self._check_blocks(agent, task, user, customer)
342
+
343
+ start = time.monotonic()
344
+ req = self._provider_request(provider, model, messages, llm, stream)
345
+
346
+ def track_usage(input_tokens: int, output_tokens: int) -> None:
347
+ self.track(
348
+ agent=agent,
349
+ task=task,
350
+ user_id=user,
351
+ customer_id=customer,
352
+ provider=provider,
353
+ model=model,
354
+ input_tokens=input_tokens,
355
+ output_tokens=output_tokens,
356
+ latency_ms=int((time.monotonic() - start) * 1000),
357
+ session_id=session_id,
358
+ input_hash=self._hash_messages(messages),
359
+ )
360
+
361
+ if stream:
362
+ try:
363
+ res = urllib.request.urlopen(req, timeout=_LLM_TIMEOUT)
364
+ except urllib.error.HTTPError as err:
365
+ detail = err.read().decode("utf-8", errors="replace")[:500]
366
+ raise RuntimeError(f"{provider} streaming request failed ({err.code}): {detail}") from err
367
+
368
+ def event_stream() -> Generator[Dict[str, Any], None, None]:
369
+ input_tokens = 0
370
+ output_tokens = 0
371
+ try:
372
+ for event in _iter_sse(res):
373
+ if provider == "anthropic":
374
+ if event.get("type") == "message_start":
375
+ input_tokens = event.get("message", {}).get("usage", {}).get("input_tokens", 0)
376
+ elif event.get("type") == "message_delta":
377
+ usage = event.get("usage") or {}
378
+ if isinstance(usage.get("output_tokens"), int):
379
+ output_tokens = usage["output_tokens"]
380
+ elif provider == "openai":
381
+ usage = event.get("usage") or {}
382
+ if isinstance(usage.get("prompt_tokens"), int):
383
+ input_tokens = usage["prompt_tokens"]
384
+ output_tokens = usage.get("completion_tokens", 0)
385
+ elif provider == "gemini":
386
+ usage = event.get("usageMetadata") or {}
387
+ if isinstance(usage.get("promptTokenCount"), int):
388
+ input_tokens = usage["promptTokenCount"]
389
+ output_tokens = usage.get("candidatesTokenCount", 0)
390
+ yield event
391
+ finally:
392
+ res.close()
393
+ # tracks whatever usage was observed, even on early termination
394
+ track_usage(input_tokens, output_tokens)
395
+
396
+ return event_stream()
397
+
398
+ try:
399
+ with urllib.request.urlopen(req, timeout=_LLM_TIMEOUT) as res:
400
+ response = json.loads(res.read().decode("utf-8"))
401
+ except urllib.error.HTTPError as err:
402
+ # Provider returned an error body — surface it as the response, like the TS SDK
403
+ try:
404
+ response = json.loads(err.read().decode("utf-8"))
405
+ except Exception:
406
+ raise RuntimeError(f"{provider} request failed ({err.code})") from err
407
+
408
+ if provider == "anthropic":
409
+ usage = response.get("usage") or {}
410
+ track_usage(usage.get("input_tokens", 0), usage.get("output_tokens", 0))
411
+ elif provider == "openai":
412
+ usage = response.get("usage") or {}
413
+ track_usage(usage.get("prompt_tokens", 0), usage.get("completion_tokens", 0))
414
+ else: # gemini
415
+ usage = response.get("usageMetadata") or {}
416
+ track_usage(usage.get("promptTokenCount", 0), usage.get("candidatesTokenCount", 0))
417
+
418
+ return response
@@ -0,0 +1,25 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "useagentledger"
7
+ version = "0.1.0"
8
+ description = "AgentLedger Python SDK — track, attribute, and govern AI agent spend across OpenAI, Anthropic, and Gemini."
9
+ readme = "README.md"
10
+ requires-python = ">=3.9"
11
+ license = { text = "MIT" }
12
+ keywords = ["llm", "cost-tracking", "ai-agents", "openai", "anthropic", "gemini", "observability"]
13
+ classifiers = [
14
+ "Development Status :: 4 - Beta",
15
+ "Intended Audience :: Developers",
16
+ "Programming Language :: Python :: 3",
17
+ "Topic :: Software Development :: Libraries",
18
+ ]
19
+
20
+ [project.urls]
21
+ Homepage = "https://useagentledger.com"
22
+ Documentation = "https://useagentledger.com/blog"
23
+
24
+ [tool.setuptools.packages.find]
25
+ include = ["agentledger*"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,141 @@
1
+ Metadata-Version: 2.4
2
+ Name: useagentledger
3
+ Version: 0.1.0
4
+ Summary: AgentLedger Python SDK — track, attribute, and govern AI agent spend across OpenAI, Anthropic, and Gemini.
5
+ License: MIT
6
+ Project-URL: Homepage, https://useagentledger.com
7
+ Project-URL: Documentation, https://useagentledger.com/blog
8
+ Keywords: llm,cost-tracking,ai-agents,openai,anthropic,gemini,observability
9
+ Classifier: Development Status :: 4 - Beta
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Topic :: Software Development :: Libraries
13
+ Requires-Python: >=3.9
14
+ Description-Content-Type: text/markdown
15
+
16
+ # AgentLedger Python SDK
17
+
18
+ Track, attribute, and govern AI agent spend across OpenAI, Anthropic, and Gemini — with per-agent / per-task / per-user / per-customer cost attribution, kill switches, loop detection, and ROI reporting.
19
+
20
+ Zero dependencies. Python 3.9+.
21
+
22
+ ```bash
23
+ pip install useagentledger
24
+ ```
25
+
26
+ (The import name is `agentledger`: `from agentledger import AgentLedger`.)
27
+
28
+ ## Quick start
29
+
30
+ ```python
31
+ from agentledger import AgentLedger
32
+
33
+ al = AgentLedger(api_key="al_live_...") # provider keys read from env by default
34
+
35
+ response = al.run(
36
+ agent="invoice-processor",
37
+ task="extraction",
38
+ customer="acme-corp",
39
+ llm={
40
+ "provider": "anthropic",
41
+ "model": "claude-sonnet-4-6",
42
+ "max_tokens": 1024,
43
+ "messages": [{"role": "user", "content": "Extract line items from this invoice: ..."}],
44
+ },
45
+ )
46
+ print(response["content"][0]["text"])
47
+ ```
48
+
49
+ Every call is tracked automatically — token counts, latency, and attribution dimensions. Cost is computed server-side from AgentLedger's pricing table (134+ models), so the SDK never goes stale.
50
+
51
+ ## Kill switch
52
+
53
+ `run()` checks AgentLedger's block list before every call (cached 5 seconds). If an alert rule has blocked this agent, task, user, or customer, the call raises **before** any provider spend happens:
54
+
55
+ ```python
56
+ from agentledger import AgentLedger, AgentLedgerBlockError
57
+
58
+ try:
59
+ al.run(agent="support-bot", customer="acme-corp", llm={...})
60
+ except AgentLedgerBlockError as e:
61
+ print(f"Blocked: {e} (dimension={e.dimension}, value={e.dimension_value})")
62
+ ```
63
+
64
+ The check **fails open** — if AgentLedger is unreachable, your LLM call proceeds normally. Governance never becomes a point of failure.
65
+
66
+ ## Streaming
67
+
68
+ Pass `stream=True` and iterate the returned generator. Usage is captured from the stream's own events and tracked when the stream ends (even if you stop early):
69
+
70
+ ```python
71
+ stream = al.run(
72
+ agent="chat-assistant",
73
+ user="user-42",
74
+ llm={
75
+ "provider": "openai",
76
+ "model": "gpt-4o",
77
+ "stream": True,
78
+ "messages": [{"role": "user", "content": "Hello!"}],
79
+ },
80
+ )
81
+ for event in stream:
82
+ delta = event.get("choices", [{}])[0].get("delta", {}).get("content")
83
+ if delta:
84
+ print(delta, end="", flush=True)
85
+ ```
86
+
87
+ Works for all three providers (Anthropic `message_start`/`message_delta` events, OpenAI usage chunk, Gemini `usageMetadata`).
88
+
89
+ ## Sessions (multi-step agents)
90
+
91
+ Group a multi-step task into one session so loop detection and per-session cost work across steps:
92
+
93
+ ```python
94
+ session_id = al.start_session(agent="research-bot", task="report")
95
+ for step in plan:
96
+ al.run(agent="research-bot", session_id=session_id, llm={...})
97
+ al.end_session(session_id, success=True)
98
+ ```
99
+
100
+ Calls without an explicit session are auto-grouped server-side (same agent + task within a 10-minute window).
101
+
102
+ ## Track calls you make yourself
103
+
104
+ Already using the official `openai` / `anthropic` clients? Just report usage:
105
+
106
+ ```python
107
+ result = openai_client.chat.completions.create(model="gpt-4o-mini", messages=msgs)
108
+
109
+ al.track(
110
+ agent="summarizer",
111
+ customer="acme-corp",
112
+ provider="openai",
113
+ model="gpt-4o-mini",
114
+ input_tokens=result.usage.prompt_tokens,
115
+ output_tokens=result.usage.completion_tokens,
116
+ )
117
+ ```
118
+
119
+ `track()` runs on a background thread by default (zero added latency) and swallows all errors. Pass `background=False` to send synchronously.
120
+
121
+ ## ROI units
122
+
123
+ Record completed business outcomes for cost-per-outcome reporting (`custom_event` count method):
124
+
125
+ ```python
126
+ al.record_unit(agent="invoice-processor", unit_count=1)
127
+ ```
128
+
129
+ ## Configuration
130
+
131
+ ```python
132
+ al = AgentLedger(
133
+ api_key="al_live_...",
134
+ base_url="https://api.useagentledger.com", # or your self-hosted proxy
135
+ provider_keys={ # optional — env vars used otherwise
136
+ "anthropic": "...", # ANTHROPIC_API_KEY
137
+ "openai": "...", # OPENAI_API_KEY
138
+ "gemini": "...", # GOOGLE_API_KEY
139
+ },
140
+ )
141
+ ```
@@ -0,0 +1,7 @@
1
+ README.md
2
+ pyproject.toml
3
+ agentledger/__init__.py
4
+ useagentledger.egg-info/PKG-INFO
5
+ useagentledger.egg-info/SOURCES.txt
6
+ useagentledger.egg-info/dependency_links.txt
7
+ useagentledger.egg-info/top_level.txt
@@ -0,0 +1 @@
1
+ agentledger