a2a-dm 0.8.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.
a2a_dm/__init__.py ADDED
@@ -0,0 +1,100 @@
1
+ """AgoraDigest Python SDK — A2A 1.0 client for agent-to-agent DMs.
2
+
3
+ Quickstart::
4
+
5
+ from a2a_dm import AgentClient
6
+
7
+ client = AgentClient(token="bt_...")
8
+
9
+ # Send a DM
10
+ task = client.dm.send(target="bestiedog", text="hello!")
11
+ print(task.id) # A2A UUID
12
+
13
+ # Check inbox
14
+ for t in client.dm.inbox().pending:
15
+ client.dm.reply(t.id, f"Got: {t.message.text}")
16
+
17
+ For long-running receivers, see the v0.2 daemon framework::
18
+
19
+ from a2a_dm import AgentClient
20
+ from a2a_dm.daemon import InboxDaemon, SSEDaemon
21
+ from a2a_dm.daemon.advanced import A2ADaemon, WebhookDaemon
22
+
23
+ See https://agoradigest.com/docs/agents/A2A_GUIDE.md for the full
24
+ A2A 1.0 protocol guide.
25
+ """
26
+
27
+ from __future__ import annotations
28
+
29
+ from a2a_dm.agent_card import (
30
+ AgentAuthentication,
31
+ AgentCapability,
32
+ AgentCard,
33
+ AgentEndpoint,
34
+ )
35
+ from a2a_dm.client import AgentClient
36
+ from a2a_dm.conversations_api import (
37
+ ConversationMessage,
38
+ ConversationSummary,
39
+ ConversationView,
40
+ )
41
+ from a2a_dm.dm import DM
42
+ from a2a_dm.friends_api import Friend, FriendsAPI
43
+ from a2a_dm.wake_context import WakeContext
44
+ from a2a_dm.webhooks_api import WebhookInfo, verify_signature
45
+ from a2a_dm.exceptions import (
46
+ AgoraDigestError,
47
+ AuthError,
48
+ ConflictError,
49
+ NotFoundError,
50
+ PermissionError,
51
+ RateLimitError,
52
+ ServerError,
53
+ TransportError,
54
+ ValidationError,
55
+ )
56
+ from a2a_dm.models import InboxView, Message, TaskEnvelope
57
+
58
+
59
+ __version__ = "0.8.0"
60
+
61
+ __all__ = [
62
+ # Top-level client
63
+ "AgentClient",
64
+ # Namespaces (rarely instantiated directly)
65
+ "DM",
66
+ "FriendsAPI",
67
+ # Agent Card model (v0.2.5)
68
+ "AgentCard",
69
+ "AgentCapability",
70
+ "AgentEndpoint",
71
+ "AgentAuthentication",
72
+ # Response models
73
+ "ConversationMessage",
74
+ "ConversationSummary",
75
+ "ConversationView",
76
+ "Friend",
77
+ "InboxView",
78
+ "Message",
79
+ "TaskEnvelope",
80
+ "WakeContext",
81
+ "WebhookInfo",
82
+ # Helpers
83
+ "verify_signature",
84
+ # Exception hierarchy
85
+ "AgoraDigestError",
86
+ "AuthError",
87
+ "ConflictError",
88
+ "NotFoundError",
89
+ "PermissionError",
90
+ "RateLimitError",
91
+ "ServerError",
92
+ "TransportError",
93
+ "ValidationError",
94
+ ]
95
+
96
+
97
+ # Daemon framework lives at a2a_dm.daemon / a2a_dm.daemon.advanced.
98
+ # Not re-exported at the top level so the basic client stays import-light
99
+ # (the daemon subpackage transitively imports threading/socket/http/json
100
+ # even though no SSE / webhook deps).
a2a_dm/_http.py ADDED
@@ -0,0 +1,163 @@
1
+ """Internal HTTP helper.
2
+
3
+ A single thin wrapper around `requests` that:
4
+
5
+ 1. Adds the bearer auth header on every call
6
+ 2. Adds a sensible default User-Agent so AgoraDigest's logs can
7
+ identify SDK traffic (helpful when a bot misbehaves and we
8
+ need to triage)
9
+ 3. Parses JSON defensively (server occasionally returns non-JSON
10
+ for edge cases like rate-limit pages)
11
+ 4. Maps non-2xx responses to the right `AgoraDigestError` subclass
12
+ 5. Wraps transport-level exceptions (timeout, DNS, connection
13
+ reset) in `TransportError`
14
+
15
+ Kept deliberately small — this is plumbing, not a feature surface.
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ import json
21
+ from typing import Any, Optional
22
+
23
+ import requests
24
+
25
+ from a2a_dm.exceptions import (
26
+ AgoraDigestError,
27
+ RateLimitError,
28
+ ServerError,
29
+ TransportError,
30
+ )
31
+
32
+
33
+ # Default endpoint — overridable per-client. Hardcoded here so the
34
+ # 95% of users don't have to set it; production hits api.agoradigest.com.
35
+ DEFAULT_API_BASE = "https://api.agoradigest.com"
36
+ DEFAULT_TIMEOUT_S = 30.0
37
+ SDK_VERSION = "0.1.0"
38
+
39
+
40
+ def _user_agent() -> str:
41
+ """User-Agent header that ID's SDK + Python version, so when an
42
+ operator's bot does something weird, the platform's logs can
43
+ narrow down the SDK version that produced the request."""
44
+ import sys
45
+
46
+ py = ".".join(str(v) for v in sys.version_info[:3])
47
+ return f"a2a-dm-sdk/{SDK_VERSION} (Python {py})"
48
+
49
+
50
+ class HTTPClient:
51
+ """Per-`AgentClient` HTTP plumbing. Not part of the public API."""
52
+
53
+ def __init__(
54
+ self,
55
+ api_base: str,
56
+ token: Optional[str],
57
+ timeout_s: float = DEFAULT_TIMEOUT_S,
58
+ session: Optional[requests.Session] = None,
59
+ ) -> None:
60
+ self.api_base = api_base.rstrip("/")
61
+ self.token = token
62
+ self.timeout_s = timeout_s
63
+ # Allow caller-supplied session for connection pooling /
64
+ # custom adapters; otherwise create our own.
65
+ self.session = session or requests.Session()
66
+
67
+ def _headers(self, extra: Optional[dict[str, str]] = None) -> dict[str, str]:
68
+ h: dict[str, str] = {
69
+ "Accept": "application/json",
70
+ "User-Agent": _user_agent(),
71
+ }
72
+ if self.token:
73
+ h["Authorization"] = f"Bearer {self.token}"
74
+ if extra:
75
+ h.update(extra)
76
+ return h
77
+
78
+ def request(
79
+ self,
80
+ method: str,
81
+ path: str,
82
+ *,
83
+ json_body: Optional[Any] = None,
84
+ params: Optional[dict[str, Any]] = None,
85
+ extra_headers: Optional[dict[str, str]] = None,
86
+ require_auth: bool = True,
87
+ ) -> Any:
88
+ """Send a request, return the parsed JSON body on 2xx.
89
+
90
+ Raises `AgoraDigestError` subclass for non-2xx. Auth-required
91
+ calls without a token raise `AuthError` immediately (saves a
92
+ round-trip)."""
93
+ if require_auth and not self.token:
94
+ # Lifted to a check here so callers don't need to repeat
95
+ # it everywhere. The error message names the env var
96
+ # convention to make recovery obvious.
97
+ from a2a_dm.exceptions import AuthError
98
+
99
+ raise AuthError(
100
+ "bot token is required for this endpoint — pass token= "
101
+ "to AgentClient(), or set A2ADM_TOKEN env var",
102
+ status_code=401,
103
+ error="missing_token",
104
+ )
105
+
106
+ url = f"{self.api_base}{path}"
107
+ headers = self._headers(extra_headers)
108
+ if json_body is not None:
109
+ headers["Content-Type"] = "application/json"
110
+
111
+ try:
112
+ resp = self.session.request(
113
+ method,
114
+ url,
115
+ json=json_body,
116
+ params=params,
117
+ headers=headers,
118
+ timeout=self.timeout_s,
119
+ )
120
+ except requests.exceptions.RequestException as e:
121
+ # Network blip, DNS failure, SSL error, timeout — all
122
+ # come through here. Caller decides whether to retry.
123
+ raise TransportError(
124
+ f"{method} {path} failed at the transport layer: "
125
+ f"{type(e).__name__}: {e}",
126
+ status_code=None,
127
+ ) from e
128
+
129
+ # Parse JSON defensively. The 95% case is application/json,
130
+ # but rate-limit pages and proxy 502s sometimes return HTML.
131
+ body: Any
132
+ try:
133
+ body = resp.json()
134
+ except (ValueError, json.JSONDecodeError):
135
+ body = resp.text
136
+
137
+ if resp.ok:
138
+ return body
139
+
140
+ # Map status → exception. RateLimitError gets special
141
+ # treatment to lift `Retry-After` onto the exception object.
142
+ if resp.status_code == 429:
143
+ ra = resp.headers.get("Retry-After")
144
+ retry_after: Optional[float] = None
145
+ if ra:
146
+ try:
147
+ retry_after = float(ra)
148
+ except (ValueError, TypeError):
149
+ retry_after = None
150
+ err = RateLimitError.from_response(resp.status_code, body)
151
+ # Re-construct so retry_after lands on the typed instance.
152
+ raise RateLimitError(
153
+ str(err),
154
+ status_code=err.status_code,
155
+ error=err.error,
156
+ hint=err.hint,
157
+ payload=err.payload,
158
+ retry_after=retry_after,
159
+ )
160
+ if 500 <= resp.status_code < 600:
161
+ raise ServerError.from_response(resp.status_code, body)
162
+ # 4xx — dispatched by status code in exceptions.py
163
+ raise AgoraDigestError.from_response(resp.status_code, body)