aforo-agent-metering 0.3.2__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.
- aforo_agent_metering-0.3.2/MANIFEST.in +4 -0
- aforo_agent_metering-0.3.2/PKG-INFO +145 -0
- aforo_agent_metering-0.3.2/README.md +116 -0
- aforo_agent_metering-0.3.2/aforo_agent_metering/__init__.py +40 -0
- aforo_agent_metering-0.3.2/aforo_agent_metering/buffer.py +43 -0
- aforo_agent_metering-0.3.2/aforo_agent_metering/client.py +704 -0
- aforo_agent_metering-0.3.2/aforo_agent_metering/decorators.py +122 -0
- aforo_agent_metering-0.3.2/aforo_agent_metering/py.typed +0 -0
- aforo_agent_metering-0.3.2/aforo_agent_metering/transport.py +254 -0
- aforo_agent_metering-0.3.2/aforo_agent_metering.egg-info/PKG-INFO +145 -0
- aforo_agent_metering-0.3.2/aforo_agent_metering.egg-info/SOURCES.txt +22 -0
- aforo_agent_metering-0.3.2/aforo_agent_metering.egg-info/dependency_links.txt +1 -0
- aforo_agent_metering-0.3.2/aforo_agent_metering.egg-info/requires.txt +10 -0
- aforo_agent_metering-0.3.2/aforo_agent_metering.egg-info/top_level.txt +1 -0
- aforo_agent_metering-0.3.2/pyproject.toml +54 -0
- aforo_agent_metering-0.3.2/setup.cfg +16 -0
- aforo_agent_metering-0.3.2/tests/test_bugfixes.py +250 -0
- aforo_agent_metering-0.3.2/tests/test_decorator.py +260 -0
- aforo_agent_metering-0.3.2/tests/test_drop.py +193 -0
- aforo_agent_metering-0.3.2/tests/test_ingest_contract.py +237 -0
- aforo_agent_metering-0.3.2/tests/test_limits.py +175 -0
- aforo_agent_metering-0.3.2/tests/test_request_label_truncation.py +98 -0
- aforo_agent_metering-0.3.2/tests/test_retry.py +165 -0
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: aforo-agent-metering
|
|
3
|
+
Version: 0.3.2
|
|
4
|
+
Summary: Aforo AI Agent Metering SDK — instrument agent runs (sessions, steps, capability calls) for billing and analytics
|
|
5
|
+
Author-email: Aforo <engineering@aforo.ai>
|
|
6
|
+
License: Apache-2.0
|
|
7
|
+
Project-URL: Homepage, https://github.com/aforoai/SDKs
|
|
8
|
+
Project-URL: Repository, https://github.com/aforoai/SDKs
|
|
9
|
+
Project-URL: Documentation, https://docs.aforo.ai
|
|
10
|
+
Keywords: aforo,ai-agent,agent,metering,billing,langchain,llamaindex,crewai,autogen,fastapi
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
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
|
+
Requires-Python: >=3.9
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
Provides-Extra: aiohttp
|
|
23
|
+
Requires-Dist: aiohttp>=3.8; extra == "aiohttp"
|
|
24
|
+
Provides-Extra: httpx
|
|
25
|
+
Requires-Dist: httpx>=0.24; extra == "httpx"
|
|
26
|
+
Provides-Extra: test
|
|
27
|
+
Requires-Dist: pytest>=7; extra == "test"
|
|
28
|
+
Requires-Dist: pytest-asyncio>=0.21; extra == "test"
|
|
29
|
+
|
|
30
|
+
# aforo-agent-metering
|
|
31
|
+
|
|
32
|
+
Meter AI agent runs from Python: record each capability invocation and reasoning step as an `AI_AGENT` usage event, buffered and batched to Aforo. For agent runtimes such as LangChain, LlamaIndex, CrewAI, AutoGen and FastAPI-hosted agents. Python sibling of `@aforoai/agent-metering` (Node).
|
|
33
|
+
|
|
34
|
+
**Version:** 0.3.2 · [Changelog](CHANGELOG.md) · [User guide](USER_GUIDE.md)
|
|
35
|
+
|
|
36
|
+
## Install
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
pip install aforo-agent-metering
|
|
40
|
+
# pick an async HTTP client (optional — stdlib urllib is the fallback):
|
|
41
|
+
pip install "aforo-agent-metering[aiohttp]"
|
|
42
|
+
pip install "aforo-agent-metering[httpx]"
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
From source:
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
git clone https://github.com/aforoai/SDKs.git
|
|
49
|
+
cd SDKs/aforo-metering-sdks/python-agent
|
|
50
|
+
pip install -e .
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Quickstart
|
|
54
|
+
|
|
55
|
+
```python
|
|
56
|
+
import asyncio
|
|
57
|
+
import os
|
|
58
|
+
from aforo_agent_metering import AforoAgentClient
|
|
59
|
+
|
|
60
|
+
async def main():
|
|
61
|
+
client = AforoAgentClient(
|
|
62
|
+
tenant_id="tenant_xxx",
|
|
63
|
+
product_id="prod_ai_001",
|
|
64
|
+
api_key=os.environ["AFORO_API_KEY"],
|
|
65
|
+
# ingestor_url defaults to https://api.aforo.ai
|
|
66
|
+
)
|
|
67
|
+
await client.start() # begin the periodic flush task
|
|
68
|
+
|
|
69
|
+
client.record_capability( # synchronous: buffers the event
|
|
70
|
+
capability_name="summarize_email",
|
|
71
|
+
agent_id="agt_001",
|
|
72
|
+
customer_id="cust_42",
|
|
73
|
+
session_id="sess_1",
|
|
74
|
+
input_tokens=320,
|
|
75
|
+
output_tokens=84,
|
|
76
|
+
execution_duration_ms=510,
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
await client.shutdown() # final flush
|
|
80
|
+
|
|
81
|
+
asyncio.run(main())
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Events POST to `https://api.aforo.ai/v1/ingest/batch` as `{"events": [...]}` with `X-API-Key: <api_key>` and `X-Tenant-Id: <tenant_id>`. No `Authorization` header is sent.
|
|
85
|
+
|
|
86
|
+
## Decorator
|
|
87
|
+
|
|
88
|
+
```python
|
|
89
|
+
from aforo_agent_metering import AforoAgentClient, wrap_capability_handler
|
|
90
|
+
|
|
91
|
+
client = AforoAgentClient(...)
|
|
92
|
+
|
|
93
|
+
@wrap_capability_handler(client, capability_name="summarize_email")
|
|
94
|
+
async def summarize_email(text: str, *, agent_id: str, session_id: str, customer_id: str):
|
|
95
|
+
...
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
The decorator times the handler, records one invocation when it finishes, and re-raises whatever the handler raised. Status: returned → `SUCCESS`; `TimeoutError` → `TIMEOUT`; any other exception → `ERROR`; `asyncio.CancelledError` / `KeyboardInterrupt` / `SystemExit` → `CANCELLED`. The handler must be `async def`.
|
|
99
|
+
|
|
100
|
+
## Configuration
|
|
101
|
+
|
|
102
|
+
| Option | Type | Default | What it does |
|
|
103
|
+
|---|---|---|---|
|
|
104
|
+
| `tenant_id` | `str` | — (required) | Aforo tenant; sent as `X-Tenant-Id`. |
|
|
105
|
+
| `product_id` | `str` | — (required) | AI_AGENT product the events bill against; stamped in `metadata.productId`. |
|
|
106
|
+
| `api_key` | `str` | — (required) | Aforo API key, sent as `X-API-Key`. |
|
|
107
|
+
| `ingestor_url` | `str` | `https://api.aforo.ai` | Host; `/v1/ingest/batch` is appended. |
|
|
108
|
+
| `default_customer_id` | `str?` | `None` | `customerId` when a call passes none. Falls back to `agent_id`. |
|
|
109
|
+
| `product_type` | `str` | `"AI_AGENT"` | Top-level `productType` on every event (trimmed + upper-cased). Override per event with `record_capability(..., product_type=...)`. |
|
|
110
|
+
| `flush_count` | `int` | `50` | Buffered events that trigger a flush (clamped to 1000, the ingestor's batch limit). |
|
|
111
|
+
| `flush_interval_sec` | `float` | `5.0` | Periodic flush cadence. Requires `await start()`. |
|
|
112
|
+
| `max_retries` | `int` | `3` | Attempts per batch. |
|
|
113
|
+
| `on_error` | `Callable[[Exception], None]?` | logs a WARN | Called on a permanent batch failure. |
|
|
114
|
+
| `on_drop` | `Callable[[list[dict], str], None]?` | `None` | Opt-in hook for events the SDK loses. |
|
|
115
|
+
| `post_fn` | `PostFn?` | aiohttp → httpx → urllib | HTTP transport override, for tests. |
|
|
116
|
+
|
|
117
|
+
### Execution status
|
|
118
|
+
|
|
119
|
+
`execution_status` is trimmed and upper-cased. Accepted values: `SUCCESS`, `PARTIAL`, `TIMEOUT`, `ERROR`, `VALIDATION_FAILED`, `FAILED`, `FAILURE`, `CANCELLED`, `PENDING`, `BLOCKED`, `HITL_REQUIRED` (exported as `EXECUTION_STATUSES`). A blank or unknown value is logged and left off; the event is still sent. OUTCOME_BASED rate plans bill each event at the weight set for its status — see the [repository README](../README.md#reporting-the-request-outcome-executionstatus).
|
|
120
|
+
|
|
121
|
+
### Retries and dropped events
|
|
122
|
+
|
|
123
|
+
Retry: 5xx, 408, 429 and network errors, with `1s / 2s` backoff; a 429 waits for its `Retry-After`. Any other 4xx is not retried.
|
|
124
|
+
|
|
125
|
+
`record_capability` and `record_step` never raise for event content. Every event the SDK cannot deliver is counted in `client.dropped_count`, WARN-logged, and passed to `on_drop(events, reason)`. Dropped events keep their idempotency keys.
|
|
126
|
+
|
|
127
|
+
| `reason` | When |
|
|
128
|
+
|---|---|
|
|
129
|
+
| `retry_exhausted` | The batch still failed after `max_retries` attempts. |
|
|
130
|
+
| `rejected` | A non-retryable 4xx for the batch (`on_error` gets the ingestor's `errors[].message`), a batch that could not be JSON-encoded, or events the ingestor rejected individually in a `202`. For a `202`, only the events the response names by index are passed to the hook; if it names none, they are counted only. |
|
|
131
|
+
| `invalid` | The SDK refused the event before buffering it: blank `capability_name` / `agent_id` (or `session_id` / `step_type` for a step), `agent_id` over 36 characters, `session_id`, `customer_id` or `capability_name` over 64. Nothing passed to `record_*` or to the decorator is truncated. The one exception: a capability name that `wrap_capability_handler` reads from the wrapped call's `capability_name` kwarg is truncated to 64 and the event is still sent. The WARN names the field and limit; it is logged on the first occurrence and then every 1000th. |
|
|
132
|
+
|
|
133
|
+
## Wire format
|
|
134
|
+
|
|
135
|
+
Each event carries top-level `customerId`, `metricName` (`ai_agent.capability_invocations` or `ai_agent.steps`), `quantity` 1, `occurredAt`, `idempotencyKey` (`agent:<uuid4>`, minted once when the event is created), `productType`, `agentId`, `sessionId`, `executionDurationMs`, optional `executionStatus`, and `metadata` with `capability_name` (snake_case — the key the ingestor maps to the per-capability billing dimension), token counts and `sdkVersion`.
|
|
136
|
+
|
|
137
|
+
The endpoint path and body shape are checked against `contract/ingest-contract.json` by this package's tests.
|
|
138
|
+
|
|
139
|
+
## Walk me through it
|
|
140
|
+
|
|
141
|
+
Install → construct → record → confirm the event in Aforo: **[USER_GUIDE.md](USER_GUIDE.md)**. A runnable example is in [`examples/hello_agent`](examples/hello_agent).
|
|
142
|
+
|
|
143
|
+
## What this doesn't cover
|
|
144
|
+
|
|
145
|
+
This SDK emits usage events. It does not send session heartbeats, check entitlements or quotas, or price anything — rate plans and per-capability pricing are configured in the Aforo console. `record_capability` / `record_step` only buffer; without `await start()` or an explicit `await flush()` nothing is sent until the buffer reaches `flush_count` inside a running event loop.
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# aforo-agent-metering
|
|
2
|
+
|
|
3
|
+
Meter AI agent runs from Python: record each capability invocation and reasoning step as an `AI_AGENT` usage event, buffered and batched to Aforo. For agent runtimes such as LangChain, LlamaIndex, CrewAI, AutoGen and FastAPI-hosted agents. Python sibling of `@aforoai/agent-metering` (Node).
|
|
4
|
+
|
|
5
|
+
**Version:** 0.3.2 · [Changelog](CHANGELOG.md) · [User guide](USER_GUIDE.md)
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pip install aforo-agent-metering
|
|
11
|
+
# pick an async HTTP client (optional — stdlib urllib is the fallback):
|
|
12
|
+
pip install "aforo-agent-metering[aiohttp]"
|
|
13
|
+
pip install "aforo-agent-metering[httpx]"
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
From source:
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
git clone https://github.com/aforoai/SDKs.git
|
|
20
|
+
cd SDKs/aforo-metering-sdks/python-agent
|
|
21
|
+
pip install -e .
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Quickstart
|
|
25
|
+
|
|
26
|
+
```python
|
|
27
|
+
import asyncio
|
|
28
|
+
import os
|
|
29
|
+
from aforo_agent_metering import AforoAgentClient
|
|
30
|
+
|
|
31
|
+
async def main():
|
|
32
|
+
client = AforoAgentClient(
|
|
33
|
+
tenant_id="tenant_xxx",
|
|
34
|
+
product_id="prod_ai_001",
|
|
35
|
+
api_key=os.environ["AFORO_API_KEY"],
|
|
36
|
+
# ingestor_url defaults to https://api.aforo.ai
|
|
37
|
+
)
|
|
38
|
+
await client.start() # begin the periodic flush task
|
|
39
|
+
|
|
40
|
+
client.record_capability( # synchronous: buffers the event
|
|
41
|
+
capability_name="summarize_email",
|
|
42
|
+
agent_id="agt_001",
|
|
43
|
+
customer_id="cust_42",
|
|
44
|
+
session_id="sess_1",
|
|
45
|
+
input_tokens=320,
|
|
46
|
+
output_tokens=84,
|
|
47
|
+
execution_duration_ms=510,
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
await client.shutdown() # final flush
|
|
51
|
+
|
|
52
|
+
asyncio.run(main())
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Events POST to `https://api.aforo.ai/v1/ingest/batch` as `{"events": [...]}` with `X-API-Key: <api_key>` and `X-Tenant-Id: <tenant_id>`. No `Authorization` header is sent.
|
|
56
|
+
|
|
57
|
+
## Decorator
|
|
58
|
+
|
|
59
|
+
```python
|
|
60
|
+
from aforo_agent_metering import AforoAgentClient, wrap_capability_handler
|
|
61
|
+
|
|
62
|
+
client = AforoAgentClient(...)
|
|
63
|
+
|
|
64
|
+
@wrap_capability_handler(client, capability_name="summarize_email")
|
|
65
|
+
async def summarize_email(text: str, *, agent_id: str, session_id: str, customer_id: str):
|
|
66
|
+
...
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
The decorator times the handler, records one invocation when it finishes, and re-raises whatever the handler raised. Status: returned → `SUCCESS`; `TimeoutError` → `TIMEOUT`; any other exception → `ERROR`; `asyncio.CancelledError` / `KeyboardInterrupt` / `SystemExit` → `CANCELLED`. The handler must be `async def`.
|
|
70
|
+
|
|
71
|
+
## Configuration
|
|
72
|
+
|
|
73
|
+
| Option | Type | Default | What it does |
|
|
74
|
+
|---|---|---|---|
|
|
75
|
+
| `tenant_id` | `str` | — (required) | Aforo tenant; sent as `X-Tenant-Id`. |
|
|
76
|
+
| `product_id` | `str` | — (required) | AI_AGENT product the events bill against; stamped in `metadata.productId`. |
|
|
77
|
+
| `api_key` | `str` | — (required) | Aforo API key, sent as `X-API-Key`. |
|
|
78
|
+
| `ingestor_url` | `str` | `https://api.aforo.ai` | Host; `/v1/ingest/batch` is appended. |
|
|
79
|
+
| `default_customer_id` | `str?` | `None` | `customerId` when a call passes none. Falls back to `agent_id`. |
|
|
80
|
+
| `product_type` | `str` | `"AI_AGENT"` | Top-level `productType` on every event (trimmed + upper-cased). Override per event with `record_capability(..., product_type=...)`. |
|
|
81
|
+
| `flush_count` | `int` | `50` | Buffered events that trigger a flush (clamped to 1000, the ingestor's batch limit). |
|
|
82
|
+
| `flush_interval_sec` | `float` | `5.0` | Periodic flush cadence. Requires `await start()`. |
|
|
83
|
+
| `max_retries` | `int` | `3` | Attempts per batch. |
|
|
84
|
+
| `on_error` | `Callable[[Exception], None]?` | logs a WARN | Called on a permanent batch failure. |
|
|
85
|
+
| `on_drop` | `Callable[[list[dict], str], None]?` | `None` | Opt-in hook for events the SDK loses. |
|
|
86
|
+
| `post_fn` | `PostFn?` | aiohttp → httpx → urllib | HTTP transport override, for tests. |
|
|
87
|
+
|
|
88
|
+
### Execution status
|
|
89
|
+
|
|
90
|
+
`execution_status` is trimmed and upper-cased. Accepted values: `SUCCESS`, `PARTIAL`, `TIMEOUT`, `ERROR`, `VALIDATION_FAILED`, `FAILED`, `FAILURE`, `CANCELLED`, `PENDING`, `BLOCKED`, `HITL_REQUIRED` (exported as `EXECUTION_STATUSES`). A blank or unknown value is logged and left off; the event is still sent. OUTCOME_BASED rate plans bill each event at the weight set for its status — see the [repository README](../README.md#reporting-the-request-outcome-executionstatus).
|
|
91
|
+
|
|
92
|
+
### Retries and dropped events
|
|
93
|
+
|
|
94
|
+
Retry: 5xx, 408, 429 and network errors, with `1s / 2s` backoff; a 429 waits for its `Retry-After`. Any other 4xx is not retried.
|
|
95
|
+
|
|
96
|
+
`record_capability` and `record_step` never raise for event content. Every event the SDK cannot deliver is counted in `client.dropped_count`, WARN-logged, and passed to `on_drop(events, reason)`. Dropped events keep their idempotency keys.
|
|
97
|
+
|
|
98
|
+
| `reason` | When |
|
|
99
|
+
|---|---|
|
|
100
|
+
| `retry_exhausted` | The batch still failed after `max_retries` attempts. |
|
|
101
|
+
| `rejected` | A non-retryable 4xx for the batch (`on_error` gets the ingestor's `errors[].message`), a batch that could not be JSON-encoded, or events the ingestor rejected individually in a `202`. For a `202`, only the events the response names by index are passed to the hook; if it names none, they are counted only. |
|
|
102
|
+
| `invalid` | The SDK refused the event before buffering it: blank `capability_name` / `agent_id` (or `session_id` / `step_type` for a step), `agent_id` over 36 characters, `session_id`, `customer_id` or `capability_name` over 64. Nothing passed to `record_*` or to the decorator is truncated. The one exception: a capability name that `wrap_capability_handler` reads from the wrapped call's `capability_name` kwarg is truncated to 64 and the event is still sent. The WARN names the field and limit; it is logged on the first occurrence and then every 1000th. |
|
|
103
|
+
|
|
104
|
+
## Wire format
|
|
105
|
+
|
|
106
|
+
Each event carries top-level `customerId`, `metricName` (`ai_agent.capability_invocations` or `ai_agent.steps`), `quantity` 1, `occurredAt`, `idempotencyKey` (`agent:<uuid4>`, minted once when the event is created), `productType`, `agentId`, `sessionId`, `executionDurationMs`, optional `executionStatus`, and `metadata` with `capability_name` (snake_case — the key the ingestor maps to the per-capability billing dimension), token counts and `sdkVersion`.
|
|
107
|
+
|
|
108
|
+
The endpoint path and body shape are checked against `contract/ingest-contract.json` by this package's tests.
|
|
109
|
+
|
|
110
|
+
## Walk me through it
|
|
111
|
+
|
|
112
|
+
Install → construct → record → confirm the event in Aforo: **[USER_GUIDE.md](USER_GUIDE.md)**. A runnable example is in [`examples/hello_agent`](examples/hello_agent).
|
|
113
|
+
|
|
114
|
+
## What this doesn't cover
|
|
115
|
+
|
|
116
|
+
This SDK emits usage events. It does not send session heartbeats, check entitlements or quotas, or price anything — rate plans and per-capability pricing are configured in the Aforo console. `record_capability` / `record_step` only buffer; without `await start()` or an explicit `await flush()` nothing is sent until the buffer reaches `flush_count` inside a running event loop.
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""
|
|
2
|
+
aforo-agent-metering — Aforo AI Agent Metering SDK for Python.
|
|
3
|
+
|
|
4
|
+
Instrument AI agent runtimes (LangChain, LlamaIndex, CrewAI, AutoGen,
|
|
5
|
+
FastAPI-hosted agents) to bill capability invocations, steps, and
|
|
6
|
+
sessions on Aforo's AI_AGENT product type.
|
|
7
|
+
|
|
8
|
+
Usage:
|
|
9
|
+
from aforo_agent_metering import AforoAgentClient, wrap_capability_handler
|
|
10
|
+
|
|
11
|
+
client = AforoAgentClient(
|
|
12
|
+
tenant_id="tenant_xxx",
|
|
13
|
+
product_id="prod_ai_001",
|
|
14
|
+
api_key=os.environ["AFORO_API_KEY"],
|
|
15
|
+
ingestor_url="https://api.aforo.ai",
|
|
16
|
+
)
|
|
17
|
+
await client.start() # begin periodic flush
|
|
18
|
+
|
|
19
|
+
@wrap_capability_handler(client, capability_name="summarize_email")
|
|
20
|
+
async def summarize(text: str, *, agent_id: str, session_id: str, customer_id: str):
|
|
21
|
+
...
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from .client import (
|
|
25
|
+
EXECUTION_STATUSES,
|
|
26
|
+
AforoAgentClient,
|
|
27
|
+
ExecutionStatus,
|
|
28
|
+
__version__,
|
|
29
|
+
normalize_execution_status,
|
|
30
|
+
)
|
|
31
|
+
from .decorators import wrap_capability_handler
|
|
32
|
+
|
|
33
|
+
__all__ = [
|
|
34
|
+
"AforoAgentClient",
|
|
35
|
+
"EXECUTION_STATUSES",
|
|
36
|
+
"ExecutionStatus",
|
|
37
|
+
"__version__",
|
|
38
|
+
"normalize_execution_status",
|
|
39
|
+
"wrap_capability_handler",
|
|
40
|
+
]
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Bounded in-memory event buffer.
|
|
3
|
+
|
|
4
|
+
Client pushes events with :meth:`EventBuffer.add`; buffer signals when
|
|
5
|
+
it's at or above the configured high-water mark so the client can flush.
|
|
6
|
+
Drain returns a copy and empties the buffer atomically (single-threaded
|
|
7
|
+
asyncio; no lock needed because ``list.copy`` / ``list.clear`` are
|
|
8
|
+
individually atomic under the GIL and callers hold the event loop
|
|
9
|
+
between them).
|
|
10
|
+
|
|
11
|
+
The buffer is intentionally dumb — no timing, no HTTP, no retry.
|
|
12
|
+
Those live in :mod:`aforo_agent_metering.transport` and
|
|
13
|
+
:mod:`aforo_agent_metering.client`.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
from typing import Any, Dict, List
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class EventBuffer:
|
|
22
|
+
"""Bounded buffer for outgoing usage events."""
|
|
23
|
+
|
|
24
|
+
def __init__(self, max_events: int = 50) -> None:
|
|
25
|
+
if max_events <= 0:
|
|
26
|
+
raise ValueError("max_events must be > 0")
|
|
27
|
+
self._events: List[Dict[str, Any]] = []
|
|
28
|
+
self.max_events = max_events
|
|
29
|
+
|
|
30
|
+
def add(self, event: Dict[str, Any]) -> bool:
|
|
31
|
+
"""Append an event. Returns True when the buffer is at or above
|
|
32
|
+
``max_events`` and the caller should flush now."""
|
|
33
|
+
self._events.append(event)
|
|
34
|
+
return len(self._events) >= self.max_events
|
|
35
|
+
|
|
36
|
+
def drain(self) -> List[Dict[str, Any]]:
|
|
37
|
+
"""Return and clear all buffered events atomically."""
|
|
38
|
+
events = self._events[:]
|
|
39
|
+
self._events.clear()
|
|
40
|
+
return events
|
|
41
|
+
|
|
42
|
+
def __len__(self) -> int:
|
|
43
|
+
return len(self._events)
|