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 +100 -0
- a2a_dm/_http.py +163 -0
- a2a_dm/agent_card.py +463 -0
- a2a_dm/agent_card_api.py +215 -0
- a2a_dm/client.py +171 -0
- a2a_dm/conversations_api.py +288 -0
- a2a_dm/daemon/__init__.py +66 -0
- a2a_dm/daemon/_base.py +445 -0
- a2a_dm/daemon/_dedup.py +87 -0
- a2a_dm/daemon/_inbox.py +126 -0
- a2a_dm/daemon/_sse.py +399 -0
- a2a_dm/daemon/advanced/__init__.py +36 -0
- a2a_dm/daemon/advanced/_a2a.py +541 -0
- a2a_dm/daemon/advanced/_async_webhook.py +724 -0
- a2a_dm/daemon/advanced/_orchestrated.py +281 -0
- a2a_dm/daemon/advanced/_pingpong.py +97 -0
- a2a_dm/daemon/advanced/_webhook.py +1020 -0
- a2a_dm/daemon/triage.py +319 -0
- a2a_dm/dm.py +615 -0
- a2a_dm/exceptions.py +227 -0
- a2a_dm/friends_api.py +411 -0
- a2a_dm/models.py +308 -0
- a2a_dm/wake_context.py +301 -0
- a2a_dm/webhooks_api.py +229 -0
- a2a_dm-0.8.0.dist-info/METADATA +258 -0
- a2a_dm-0.8.0.dist-info/RECORD +29 -0
- a2a_dm-0.8.0.dist-info/WHEEL +5 -0
- a2a_dm-0.8.0.dist-info/licenses/LICENSE +201 -0
- a2a_dm-0.8.0.dist-info/top_level.txt +1 -0
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)
|