a2ea-py 0.1.0__py3-none-any.whl

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.
a2ea/__init__.py ADDED
@@ -0,0 +1,11 @@
1
+ """Official Python SDK for the A2EA telemetry platform."""
2
+
3
+ from .client import A2EAClient, get_default_client, set_default_client
4
+ from .decorators import track
5
+
6
+ __all__ = [
7
+ "A2EAClient",
8
+ "get_default_client",
9
+ "set_default_client",
10
+ "track",
11
+ ]
a2ea/client.py ADDED
@@ -0,0 +1,223 @@
1
+ from __future__ import annotations
2
+
3
+ import hashlib
4
+ import hmac
5
+ import logging
6
+ import os
7
+ import queue
8
+ import socket
9
+ import threading
10
+ import time
11
+ import uuid
12
+ from dataclasses import dataclass
13
+ from datetime import datetime, timezone
14
+ from typing import Any, Optional
15
+
16
+ import requests
17
+
18
+ LOGGER = logging.getLogger("a2ea.sdk")
19
+
20
+ _DEFAULT_CLIENT: "A2EAClient | None" = None
21
+
22
+
23
+ @dataclass
24
+ class TelemetryJob:
25
+ event: dict[str, Any]
26
+ task_type: str
27
+ attempt: int = 0
28
+
29
+
30
+ def set_default_client(client: "A2EAClient") -> None:
31
+ global _DEFAULT_CLIENT
32
+ _DEFAULT_CLIENT = client
33
+
34
+
35
+ def get_default_client() -> "A2EAClient | None":
36
+ return _DEFAULT_CLIENT
37
+
38
+
39
+ def _default_base_url() -> str:
40
+ return os.getenv("A2EA_BASE_URL", "https://aimodularity.com")
41
+
42
+
43
+ def _default_agent_id() -> str:
44
+ hostname = socket.gethostname().replace(".", "-")
45
+ return f"agent_{hostname}_{os.getpid()}"
46
+
47
+
48
+ class A2EAClient:
49
+ """Production-ready Python client for A2EA telemetry submission.
50
+
51
+ The client signs telemetry locally with the workspace API key, queues delivery in a
52
+ background thread, retries transient failures, and keeps a fallback buffer when the
53
+ transport path is unavailable.
54
+ """
55
+
56
+ def __init__(
57
+ self,
58
+ api_key: str,
59
+ *,
60
+ base_url: Optional[str] = None,
61
+ agent_id: Optional[str] = None,
62
+ request_timeout: float = 5.0,
63
+ max_retries: int = 5,
64
+ retry_backoff_base_seconds: float = 0.75,
65
+ max_retry_delay_seconds: float = 30.0,
66
+ auto_set_default: bool = True,
67
+ ) -> None:
68
+ if not api_key or not api_key.strip():
69
+ raise ValueError("api_key is required")
70
+
71
+ self.base_url = (base_url or _default_base_url()).rstrip("/")
72
+ self.agent_id = agent_id or _default_agent_id()
73
+ self.api_key = api_key.strip()
74
+ self.signing_secret = self.api_key.encode("utf-8")
75
+ self.signing_key_id = "workspace-api-key"
76
+ self.request_timeout = request_timeout
77
+ self.max_retries = max_retries
78
+ self.retry_backoff_base_seconds = retry_backoff_base_seconds
79
+ self.max_retry_delay_seconds = max_retry_delay_seconds
80
+ self.session = requests.Session()
81
+ self._queue: "queue.Queue[TelemetryJob]" = queue.Queue()
82
+ self._fallback_buffer: list[dict[str, Any]] = []
83
+ self._worker = threading.Thread(target=self._worker_loop, name="a2ea-worker", daemon=True)
84
+ self._worker.start()
85
+
86
+ if auto_set_default:
87
+ set_default_client(self)
88
+
89
+ def track_agent(self, *, task_type: str, agent_name: Optional[str] = None):
90
+ from .decorators import track
91
+
92
+ return track(agent_name=agent_name or self.agent_id, task_type=task_type, client=self)
93
+
94
+ def send_telemetry(
95
+ self,
96
+ task_type: str,
97
+ duration_ms: int,
98
+ outcome: str,
99
+ *,
100
+ event_id: Optional[str] = None,
101
+ timestamp: Optional[str] = None,
102
+ ) -> Optional[str]:
103
+ event = self._build_event(
104
+ task_type=task_type,
105
+ duration_ms=duration_ms,
106
+ outcome=outcome,
107
+ event_id=event_id,
108
+ timestamp=timestamp,
109
+ )
110
+ self._queue.put(TelemetryJob(event=event, task_type=task_type))
111
+ return event["eventId"]
112
+
113
+ def get_local_fallback_buffer(self) -> list[dict[str, Any]]:
114
+ return list(self._fallback_buffer)
115
+
116
+ def flush(self, timeout: Optional[float] = None) -> None:
117
+ start = time.time()
118
+ while self._queue.unfinished_tasks:
119
+ if timeout is not None and (time.time() - start) > timeout:
120
+ raise TimeoutError("A2EA telemetry queue did not flush before timeout")
121
+ time.sleep(0.05)
122
+
123
+ def close(self) -> None:
124
+ self.session.close()
125
+
126
+ def _worker_loop(self) -> None:
127
+ while True:
128
+ job = self._queue.get()
129
+ try:
130
+ self._ship_event(job.event, job.task_type)
131
+ except Exception as exc: # pragma: no cover - intentional silent fallback boundary
132
+ LOGGER.warning(
133
+ "A2EA telemetry transport failed for %s (attempt %s/%s): %s",
134
+ job.event.get("eventId"),
135
+ job.attempt + 1,
136
+ self.max_retries,
137
+ exc,
138
+ )
139
+ self._handle_retry(job)
140
+ finally:
141
+ self._queue.task_done()
142
+
143
+ def _handle_retry(self, job: TelemetryJob) -> None:
144
+ next_attempt = job.attempt + 1
145
+ if next_attempt > self.max_retries:
146
+ LOGGER.warning(
147
+ "A2EA telemetry event %s exhausted retries; retaining in local fallback buffer",
148
+ job.event.get("eventId"),
149
+ )
150
+ self._fallback_buffer.append(job.event)
151
+ return
152
+
153
+ delay = min(self.retry_backoff_base_seconds * (2 ** job.attempt), self.max_retry_delay_seconds)
154
+ timer = threading.Timer(
155
+ delay,
156
+ lambda: self._queue.put(TelemetryJob(event=job.event, task_type=job.task_type, attempt=next_attempt)),
157
+ )
158
+ timer.daemon = True
159
+ timer.start()
160
+
161
+ def _ship_event(self, event: dict[str, Any], task_type: str) -> None:
162
+ payload = {
163
+ "taskType": task_type,
164
+ "telemetry": [event],
165
+ }
166
+
167
+ response = self.session.post(
168
+ f"{self.base_url}/api/v1/telemetry/score",
169
+ json=payload,
170
+ timeout=self.request_timeout,
171
+ headers={
172
+ "X-A2EA-API-Key": self.api_key,
173
+ "Content-Type": "application/json",
174
+ },
175
+ )
176
+ if response.status_code >= 300:
177
+ raise RuntimeError(f"telemetry score request failed with status {response.status_code}: {response.text}")
178
+
179
+ def _build_event(
180
+ self,
181
+ *,
182
+ task_type: str,
183
+ duration_ms: int,
184
+ outcome: str,
185
+ event_id: Optional[str],
186
+ timestamp: Optional[str],
187
+ ) -> dict[str, Any]:
188
+ normalized_outcome = "failure" if outcome == "failure" else "success"
189
+ event = {
190
+ "eventId": event_id or f"evt_{uuid.uuid4().hex}",
191
+ "agentId": self.agent_id,
192
+ "timestamp": timestamp or datetime.now(timezone.utc).isoformat().replace("+00:00", "Z"),
193
+ "durationMs": int(duration_ms),
194
+ "taskType": task_type,
195
+ "outcome": normalized_outcome,
196
+ }
197
+ signature = self._sign_payload(event)
198
+ event["verification"] = {
199
+ "keyId": self.signing_key_id,
200
+ "algorithm": "hmac_sha256",
201
+ "signature": signature,
202
+ }
203
+ return event
204
+
205
+ def _sign_payload(self, event: dict[str, Any]) -> str:
206
+ canonical = self._canonical_event_payload(event)
207
+ return hmac.new(self.signing_secret, canonical.encode("utf-8"), hashlib.sha256).hexdigest()
208
+
209
+ @staticmethod
210
+ def _canonical_event_payload(event: dict[str, Any]) -> str:
211
+ timestamp_value = str(event["timestamp"])
212
+ if timestamp_value.endswith("Z"):
213
+ timestamp_value = timestamp_value.replace("Z", "+00:00")
214
+ timestamp = datetime.fromisoformat(timestamp_value).astimezone(timezone.utc).isoformat()
215
+
216
+ return (
217
+ f"event_id={event['eventId']}|"
218
+ f"agent_id={event['agentId']}|"
219
+ f"timestamp={timestamp}|"
220
+ f"duration_ms={event['durationMs']}|"
221
+ f"task_type={event['taskType']}|"
222
+ f"outcome={event['outcome']}"
223
+ )
a2ea/decorators.py ADDED
@@ -0,0 +1,80 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ import logging
5
+ import time
6
+ from functools import wraps
7
+ from typing import Any, Callable, Optional, TypeVar
8
+
9
+ from .client import get_default_client
10
+
11
+ LOGGER = logging.getLogger("a2ea.sdk")
12
+
13
+ F = TypeVar("F", bound=Callable[..., Any])
14
+
15
+
16
+ def track(*, agent_name: Optional[str] = None, task_type: str, client=None) -> Callable[[F], F]:
17
+ """Framework-agnostic telemetry wrapper.
18
+
19
+ Captures only execution timing and success/failure metadata. It does not inspect
20
+ function arguments or private model payloads.
21
+ """
22
+
23
+ def decorator(func: F) -> F:
24
+ if asyncio.iscoroutinefunction(func):
25
+
26
+ @wraps(func)
27
+ async def async_wrapper(*args: Any, **kwargs: Any):
28
+ started = time.perf_counter()
29
+ selected_client = client or get_default_client()
30
+ try:
31
+ result = await func(*args, **kwargs)
32
+ if selected_client:
33
+ selected_client.send_telemetry(
34
+ task_type=task_type,
35
+ duration_ms=int((time.perf_counter() - started) * 1000),
36
+ outcome="success",
37
+ )
38
+ return result
39
+ except Exception:
40
+ if selected_client:
41
+ try:
42
+ selected_client.send_telemetry(
43
+ task_type=task_type,
44
+ duration_ms=int((time.perf_counter() - started) * 1000),
45
+ outcome="failure",
46
+ )
47
+ except Exception as exc: # pragma: no cover
48
+ LOGGER.warning("A2EA async telemetry fallback failed for %s: %s", agent_name or task_type, exc)
49
+ raise
50
+
51
+ return async_wrapper # type: ignore[return-value]
52
+
53
+ @wraps(func)
54
+ def sync_wrapper(*args: Any, **kwargs: Any):
55
+ started = time.perf_counter()
56
+ selected_client = client or get_default_client()
57
+ try:
58
+ result = func(*args, **kwargs)
59
+ if selected_client:
60
+ selected_client.send_telemetry(
61
+ task_type=task_type,
62
+ duration_ms=int((time.perf_counter() - started) * 1000),
63
+ outcome="success",
64
+ )
65
+ return result
66
+ except Exception:
67
+ if selected_client:
68
+ try:
69
+ selected_client.send_telemetry(
70
+ task_type=task_type,
71
+ duration_ms=int((time.perf_counter() - started) * 1000),
72
+ outcome="failure",
73
+ )
74
+ except Exception as exc: # pragma: no cover
75
+ LOGGER.warning("A2EA sync telemetry fallback failed for %s: %s", agent_name or task_type, exc)
76
+ raise
77
+
78
+ return sync_wrapper # type: ignore[return-value]
79
+
80
+ return decorator
@@ -0,0 +1,81 @@
1
+ Metadata-Version: 2.4
2
+ Name: a2ea-py
3
+ Version: 0.1.0
4
+ Summary: Official Python client for the A2EA telemetry and attribution platform.
5
+ Author: AI Blockchain Ventures
6
+ Requires-Python: >=3.9
7
+ Description-Content-Type: text/markdown
8
+ Requires-Dist: requests>=2.31.0
9
+
10
+ # a2ea-py
11
+
12
+ Official Python SDK for sending A2EA telemetry to the backend.
13
+
14
+ ## Install
15
+
16
+ ```bash
17
+ pip install a2ea-py
18
+ ```
19
+
20
+ ## Quickstart
21
+
22
+ ```python
23
+ from a2ea import A2EAClient
24
+
25
+ client = A2EAClient(
26
+ api_key="your X-A2EA-API-Key"
27
+ )
28
+
29
+
30
+ @client.track_agent(task_type="data_processing")
31
+ def run_agent(payload):
32
+ return process(payload)
33
+ ```
34
+
35
+ ### Optional configuration
36
+
37
+ - `base_url`
38
+ - Defaults to `https://aimodularity.com`
39
+ - Set this when testing locally against `http://127.0.0.1:4080`
40
+ - `agent_id`
41
+ - Optional override if you want to assign a fixed agent identifier instead of the default host/process-based value
42
+
43
+ Example for local development:
44
+
45
+ ```python
46
+ from a2ea import A2EAClient
47
+
48
+ client = A2EAClient(
49
+ api_key="your X-A2EA-API-Key",
50
+ base_url="http://127.0.0.1:4080",
51
+ agent_id="agent_pipeline_014",
52
+ )
53
+ ```
54
+
55
+ ## What the client sends
56
+
57
+ Each tracked function call produces:
58
+
59
+ - `eventId`
60
+ - `agentId`
61
+ - `timestamp`
62
+ - `durationMs`
63
+ - `taskType`
64
+ - `outcome`
65
+ - `verification`
66
+
67
+ The verification payload is signed locally with HMAC-SHA256 using the workspace API key so the backend can verify the telemetry envelope.
68
+
69
+ ## Reliability behavior
70
+
71
+ - submission runs in a background thread
72
+ - transient failures are retried with backoff
73
+ - exhausted events are retained in a local fallback buffer
74
+
75
+ ## Local package test commands
76
+
77
+ ```bash
78
+ python -m unittest discover tests
79
+ python -m build
80
+ pip install dist/a2ea_py-0.1.0-py3-none-any.whl
81
+ ```
@@ -0,0 +1,7 @@
1
+ a2ea/__init__.py,sha256=-NaIVonjIwfexKJBn_ebOjP6aA4ZyAWTD5pGhpF8SAo,259
2
+ a2ea/client.py,sha256=81aRoHEM90a763yKJnmUAZTDLcfpNz3KzXucIEytdAM,7481
3
+ a2ea/decorators.py,sha256=9rrX9BngkaaS4awgG3e2aKDUR2r36W-0kK2Nsq30fW8,3127
4
+ a2ea_py-0.1.0.dist-info/METADATA,sha256=yVG1USUeYqugrjxUTrWn4XuiOSMsFivumglzIjWtous,1780
5
+ a2ea_py-0.1.0.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
6
+ a2ea_py-0.1.0.dist-info/top_level.txt,sha256=dZP6uZHNRs2dE5AMlwt4JmDlVSAVh_Mj08ZVaRlcLqI,5
7
+ a2ea_py-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (83.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ a2ea