uagents-core 0.3.2__tar.gz → 0.3.4__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.
- {uagents_core-0.3.2 → uagents_core-0.3.4}/PKG-INFO +1 -1
- {uagents_core-0.3.2 → uagents_core-0.3.4}/pyproject.toml +1 -1
- {uagents_core-0.3.2 → uagents_core-0.3.4}/uagents_core/config.py +5 -0
- {uagents_core-0.3.2 → uagents_core-0.3.4}/uagents_core/registration.py +4 -0
- {uagents_core-0.3.2 → uagents_core-0.3.4}/uagents_core/storage.py +47 -16
- {uagents_core-0.3.2 → uagents_core-0.3.4}/uagents_core/utils/registration.py +140 -15
- uagents_core-0.3.4/uagents_core/utils/subscriptions.py +82 -0
- {uagents_core-0.3.2 → uagents_core-0.3.4}/README.md +0 -0
- {uagents_core-0.3.2 → uagents_core-0.3.4}/uagents_core/__init__.py +0 -0
- {uagents_core-0.3.2 → uagents_core-0.3.4}/uagents_core/contrib/__init__.py +0 -0
- {uagents_core-0.3.2 → uagents_core-0.3.4}/uagents_core/contrib/protocols/__init__.py +0 -0
- {uagents_core-0.3.2 → uagents_core-0.3.4}/uagents_core/contrib/protocols/chat/__init__.py +0 -0
- {uagents_core-0.3.2 → uagents_core-0.3.4}/uagents_core/contrib/protocols/subscriptions/__init__.py +0 -0
- {uagents_core-0.3.2 → uagents_core-0.3.4}/uagents_core/envelope.py +0 -0
- {uagents_core-0.3.2 → uagents_core-0.3.4}/uagents_core/helpers.py +0 -0
- {uagents_core-0.3.2 → uagents_core-0.3.4}/uagents_core/identity.py +0 -0
- {uagents_core-0.3.2 → uagents_core-0.3.4}/uagents_core/logger.py +0 -0
- {uagents_core-0.3.2 → uagents_core-0.3.4}/uagents_core/models.py +0 -0
- {uagents_core-0.3.2 → uagents_core-0.3.4}/uagents_core/protocol.py +0 -0
- {uagents_core-0.3.2 → uagents_core-0.3.4}/uagents_core/types.py +0 -0
- {uagents_core-0.3.2 → uagents_core-0.3.4}/uagents_core/utils/__init__.py +0 -0
- {uagents_core-0.3.2 → uagents_core-0.3.4}/uagents_core/utils/messages.py +0 -0
- {uagents_core-0.3.2 → uagents_core-0.3.4}/uagents_core/utils/resolver.py +0 -0
@@ -7,6 +7,7 @@ DEFAULT_CHALLENGE_PATH = "/v1/auth/challenge"
|
|
7
7
|
DEFAULT_MAILBOX_PATH = "/v1/submit"
|
8
8
|
DEFAULT_PROXY_PATH = "/v1/proxy/submit"
|
9
9
|
DEFAULT_STORAGE_PATH = "/v1/storage"
|
10
|
+
DEFAULT_PAYMENTS_PATH = "/v1/payments"
|
10
11
|
|
11
12
|
DEFAULT_MAX_ENDPOINTS = 10
|
12
13
|
|
@@ -35,3 +36,7 @@ class AgentverseConfig(BaseModel):
|
|
35
36
|
@property
|
36
37
|
def storage_endpoint(self) -> str:
|
37
38
|
return f"{self.url}{DEFAULT_STORAGE_PATH}"
|
39
|
+
|
40
|
+
@property
|
41
|
+
def payments_endpoint(self) -> str:
|
42
|
+
return f"{self.url}{DEFAULT_PAYMENTS_PATH}"
|
@@ -73,6 +73,10 @@ class AgentRegistrationAttestation(VerifiableModel):
|
|
73
73
|
metadata: dict[str, str | list[str] | dict[str, str]] | None = None
|
74
74
|
|
75
75
|
|
76
|
+
class AgentRegistrationAttestationBatch(BaseModel):
|
77
|
+
attestations: list[AgentRegistrationAttestation]
|
78
|
+
|
79
|
+
|
76
80
|
# Agentverse related models
|
77
81
|
class RegistrationRequest(BaseModel):
|
78
82
|
address: str
|
@@ -2,7 +2,6 @@ import base64
|
|
2
2
|
import struct
|
3
3
|
from datetime import datetime
|
4
4
|
from secrets import token_bytes
|
5
|
-
from typing import Optional
|
6
5
|
|
7
6
|
import requests
|
8
7
|
|
@@ -34,38 +33,57 @@ def compute_attestation(
|
|
34
33
|
class ExternalStorage:
|
35
34
|
def __init__(
|
36
35
|
self,
|
37
|
-
|
38
|
-
|
39
|
-
|
36
|
+
*,
|
37
|
+
identity: Identity | None = None,
|
38
|
+
storage_url: str | None = None,
|
39
|
+
api_token: str | None = None,
|
40
40
|
):
|
41
41
|
self.identity = identity
|
42
42
|
self.api_token = api_token
|
43
|
+
if not (identity or api_token):
|
44
|
+
raise ValueError(
|
45
|
+
"Either an identity or an API token must be provided for authentication"
|
46
|
+
)
|
43
47
|
self.storage_url = storage_url or AgentverseConfig().storage_endpoint
|
44
48
|
|
45
49
|
def _make_attestation(self) -> str:
|
46
50
|
nonce = token_bytes(32)
|
47
51
|
now = datetime.now()
|
48
|
-
|
52
|
+
if not self.identity:
|
53
|
+
raise RuntimeError("No identity available to create attestation")
|
54
|
+
return compute_attestation(
|
55
|
+
identity=self.identity,
|
56
|
+
validity_start=now,
|
57
|
+
validity_secs=3600,
|
58
|
+
nonce=nonce,
|
59
|
+
)
|
49
60
|
|
50
61
|
def _get_auth_header(self) -> dict:
|
51
62
|
if self.api_token:
|
52
63
|
return {"Authorization": f"Bearer {self.api_token}"}
|
53
|
-
|
64
|
+
if self.identity:
|
54
65
|
return {"Authorization": f"Agent {self._make_attestation()}"}
|
55
|
-
|
56
|
-
raise RuntimeError("No identity or API token available for authentication")
|
66
|
+
raise RuntimeError("No identity or API token available for authentication")
|
57
67
|
|
58
68
|
def upload(
|
59
|
-
self,
|
69
|
+
self,
|
70
|
+
asset_id: str,
|
71
|
+
asset_content: bytes,
|
72
|
+
mime_type: str = "text/plain",
|
60
73
|
) -> dict:
|
61
74
|
url = f"{self.storage_url}/assets/{asset_id}/contents/"
|
62
75
|
headers = self._get_auth_header()
|
63
76
|
headers["Content-Type"] = "application/json"
|
64
77
|
payload = {
|
65
|
-
"contents": base64.b64encode(
|
78
|
+
"contents": base64.b64encode(asset_content).decode(),
|
66
79
|
"mime_type": mime_type,
|
67
80
|
}
|
68
|
-
response = requests.put(
|
81
|
+
response = requests.put(
|
82
|
+
url=url,
|
83
|
+
json=payload,
|
84
|
+
headers=headers,
|
85
|
+
timeout=10,
|
86
|
+
)
|
69
87
|
if response.status_code != 200:
|
70
88
|
raise RuntimeError(
|
71
89
|
f"Upload failed: {response.status_code}, {response.text}"
|
@@ -76,8 +94,11 @@ class ExternalStorage:
|
|
76
94
|
def download(self, asset_id: str) -> dict:
|
77
95
|
url = f"{self.storage_url}/assets/{asset_id}/contents/"
|
78
96
|
headers = self._get_auth_header()
|
79
|
-
|
80
|
-
|
97
|
+
response = requests.get(
|
98
|
+
url=url,
|
99
|
+
headers=headers,
|
100
|
+
timeout=10,
|
101
|
+
)
|
81
102
|
if response.status_code != 200:
|
82
103
|
raise RuntimeError(
|
83
104
|
f"Download failed: {response.status_code}, {response.text}"
|
@@ -104,7 +125,12 @@ class ExternalStorage:
|
|
104
125
|
"lifetime_hours": lifetime_hours,
|
105
126
|
}
|
106
127
|
|
107
|
-
response = requests.post(
|
128
|
+
response = requests.post(
|
129
|
+
url=url,
|
130
|
+
json=payload,
|
131
|
+
headers=headers,
|
132
|
+
timeout=10,
|
133
|
+
)
|
108
134
|
if response.status_code != 201:
|
109
135
|
raise RuntimeError(
|
110
136
|
f"Asset creation failed: {response.status_code}, {response.text}"
|
@@ -114,7 +140,7 @@ class ExternalStorage:
|
|
114
140
|
|
115
141
|
def set_permissions(
|
116
142
|
self, asset_id: str, agent_address: str, read: bool = True, write: bool = True
|
117
|
-
):
|
143
|
+
) -> dict:
|
118
144
|
if not self.api_token:
|
119
145
|
raise RuntimeError("API token required to set permissions")
|
120
146
|
url = f"{self.storage_url}/assets/{asset_id}/permissions/"
|
@@ -126,7 +152,12 @@ class ExternalStorage:
|
|
126
152
|
"write": write,
|
127
153
|
}
|
128
154
|
|
129
|
-
response = requests.put(
|
155
|
+
response = requests.put(
|
156
|
+
url=url,
|
157
|
+
json=payload,
|
158
|
+
headers=headers,
|
159
|
+
timeout=10,
|
160
|
+
)
|
130
161
|
if response.status_code != 200:
|
131
162
|
raise RuntimeError(
|
132
163
|
f"Set permissions failed: {response.status_code}, {response.text}"
|
@@ -19,6 +19,7 @@ from uagents_core.logger import get_logger
|
|
19
19
|
from uagents_core.protocol import is_valid_protocol_digest
|
20
20
|
from uagents_core.registration import (
|
21
21
|
AgentRegistrationAttestation,
|
22
|
+
AgentRegistrationAttestationBatch,
|
22
23
|
AgentStatusUpdate,
|
23
24
|
AgentUpdates,
|
24
25
|
AgentverseConnectRequest,
|
@@ -27,11 +28,33 @@ from uagents_core.registration import (
|
|
27
28
|
RegistrationRequest,
|
28
29
|
RegistrationResponse,
|
29
30
|
)
|
30
|
-
from uagents_core.types import AgentEndpoint
|
31
|
+
from uagents_core.types import AddressPrefix, AgentEndpoint
|
31
32
|
|
32
33
|
logger = get_logger("uagents_core.utils.registration")
|
33
34
|
|
34
35
|
|
36
|
+
class AgentRegistrationInput:
|
37
|
+
identity: Identity
|
38
|
+
prefix: str | None = None
|
39
|
+
endpoints: list[str]
|
40
|
+
protocol_digests: list[str]
|
41
|
+
metadata: dict[str, str | list[str] | dict[str, str]] | None = None
|
42
|
+
|
43
|
+
def __init__(
|
44
|
+
self,
|
45
|
+
identity: Identity,
|
46
|
+
endpoints: list[str],
|
47
|
+
protocol_digests: list[str],
|
48
|
+
prefix: AddressPrefix | None = None,
|
49
|
+
metadata: dict[str, str | list[str] | dict[str, str]] | None = None,
|
50
|
+
):
|
51
|
+
self.identity = identity
|
52
|
+
self.prefix = prefix
|
53
|
+
self.endpoints = endpoints
|
54
|
+
self.protocol_digests = protocol_digests
|
55
|
+
self.metadata = metadata
|
56
|
+
|
57
|
+
|
35
58
|
def _send_post_request(
|
36
59
|
url: str,
|
37
60
|
data: BaseModel,
|
@@ -63,11 +86,32 @@ def _send_post_request(
|
|
63
86
|
return False, None
|
64
87
|
|
65
88
|
|
89
|
+
def _build_signed_attestation(
|
90
|
+
item: AgentRegistrationInput,
|
91
|
+
) -> AgentRegistrationAttestation:
|
92
|
+
agent_endpoints: list[AgentEndpoint] = [
|
93
|
+
AgentEndpoint(url=endpoint, weight=1) for endpoint in item.endpoints
|
94
|
+
]
|
95
|
+
|
96
|
+
attestation = AgentRegistrationAttestation(
|
97
|
+
agent_identifier=f"{item.prefix}://{item.identity.address}"
|
98
|
+
if item.prefix
|
99
|
+
else item.identity.address,
|
100
|
+
protocols=item.protocol_digests,
|
101
|
+
endpoints=agent_endpoints,
|
102
|
+
metadata=item.metadata,
|
103
|
+
)
|
104
|
+
|
105
|
+
attestation.sign(item.identity)
|
106
|
+
return attestation
|
107
|
+
|
108
|
+
|
66
109
|
def register_in_almanac(
|
67
110
|
identity: Identity,
|
68
111
|
endpoints: list[str],
|
69
112
|
protocol_digests: list[str],
|
70
113
|
metadata: dict[str, str | list[str] | dict[str, str]] | None = None,
|
114
|
+
prefix: AddressPrefix | None = None,
|
71
115
|
*,
|
72
116
|
agentverse_config: AgentverseConfig | None = None,
|
73
117
|
timeout: int = DEFAULT_REQUEST_TIMEOUT,
|
@@ -77,6 +121,7 @@ def register_in_almanac(
|
|
77
121
|
|
78
122
|
Args:
|
79
123
|
identity (Identity): The identity of the agent.
|
124
|
+
prefix (AddressPrefix | None): The prefix for the agent identifier.
|
80
125
|
endpoints (list[str]): The endpoints that the agent can be reached at.
|
81
126
|
protocol_digests (list[str]): The digests of the protocol that the agent supports
|
82
127
|
agentverse_config (AgentverseConfig): The configuration for the agentverse API
|
@@ -95,10 +140,6 @@ def register_in_almanac(
|
|
95
140
|
)
|
96
141
|
return False
|
97
142
|
|
98
|
-
agent_endpoints: list[AgentEndpoint] = [
|
99
|
-
AgentEndpoint(url=endpoint, weight=1) for endpoint in endpoints
|
100
|
-
]
|
101
|
-
|
102
143
|
# check protocol digests
|
103
144
|
for proto_digest in protocol_digests:
|
104
145
|
if not is_valid_protocol_digest(proto_digest):
|
@@ -112,22 +153,18 @@ def register_in_almanac(
|
|
112
153
|
agentverse_config = agentverse_config or AgentverseConfig()
|
113
154
|
almanac_api = urllib.parse.urljoin(agentverse_config.url, DEFAULT_ALMANAC_API_PATH)
|
114
155
|
|
115
|
-
# get the agent address
|
116
|
-
agent_address = identity.address
|
117
|
-
|
118
156
|
# create the attestation
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
endpoints=
|
157
|
+
item = AgentRegistrationInput(
|
158
|
+
identity=identity,
|
159
|
+
prefix=prefix,
|
160
|
+
endpoints=endpoints,
|
161
|
+
protocol_digests=protocol_digests,
|
123
162
|
metadata=metadata,
|
124
163
|
)
|
164
|
+
attestation = _build_signed_attestation(item)
|
125
165
|
|
126
166
|
logger.info(msg="Registering with Almanac API", extra=attestation.model_dump())
|
127
167
|
|
128
|
-
# sign the attestation
|
129
|
-
attestation.sign(identity)
|
130
|
-
|
131
168
|
# submit the attestation to the API
|
132
169
|
status, _ = _send_post_request(
|
133
170
|
url=f"{almanac_api}/agents", data=attestation, timeout=timeout
|
@@ -135,6 +172,94 @@ def register_in_almanac(
|
|
135
172
|
return status
|
136
173
|
|
137
174
|
|
175
|
+
def register_batch_in_almanac(
|
176
|
+
items: list[AgentRegistrationInput],
|
177
|
+
*,
|
178
|
+
agentverse_config: AgentverseConfig | None = None,
|
179
|
+
timeout: int = DEFAULT_REQUEST_TIMEOUT,
|
180
|
+
validate_all_before_registration: bool = False,
|
181
|
+
) -> tuple[bool, list[str]]:
|
182
|
+
"""
|
183
|
+
Register multiple identities with the Almanac API to make them discoverable by other agents.
|
184
|
+
|
185
|
+
The return value is a 2-tuple including:
|
186
|
+
* (bool) Whether the registration request was both attempted and successful.
|
187
|
+
* (list[str]) A list of addresses of identities that failed validation.
|
188
|
+
|
189
|
+
If `validate_all_before_registration` is `True`, no registration request will be sent
|
190
|
+
unless all identities pass validation.
|
191
|
+
|
192
|
+
Args:
|
193
|
+
items (list[AgentRegistrationInput]): The list of identities to register.
|
194
|
+
See `register_in_almanac` for details about attributes in `AgentRegistrationInput`.
|
195
|
+
agentverse_config (AgentverseConfig): The configuration for the agentverse API
|
196
|
+
timeout (int): The timeout for the request
|
197
|
+
"""
|
198
|
+
invalid_identities: list[str] = []
|
199
|
+
attestations: list[AgentRegistrationAttestation] = []
|
200
|
+
|
201
|
+
for item in items:
|
202
|
+
# check endpoints
|
203
|
+
if not item.endpoints:
|
204
|
+
logger.warning(
|
205
|
+
f"No endpoints provided for {item.identity.address}; skipping registration",
|
206
|
+
)
|
207
|
+
invalid_identities.append(item.identity.address)
|
208
|
+
for endpoint in item.endpoints:
|
209
|
+
result = urllib.parse.urlparse(endpoint)
|
210
|
+
if not all([result.scheme, result.netloc]):
|
211
|
+
logger.error(
|
212
|
+
msg=f"Invalid endpoint provided for {item.identity.address}; "
|
213
|
+
+ "skipping registration",
|
214
|
+
extra={"endpoint": endpoint},
|
215
|
+
)
|
216
|
+
invalid_identities.append(item.identity.address)
|
217
|
+
|
218
|
+
# check protocol digests
|
219
|
+
for proto_digest in item.protocol_digests:
|
220
|
+
if not is_valid_protocol_digest(proto_digest):
|
221
|
+
logger.error(
|
222
|
+
msg=f"Invalid protocol digest provided for {item.identity.address}; "
|
223
|
+
+ "skipping registration",
|
224
|
+
extra={"protocol_digest": proto_digest},
|
225
|
+
)
|
226
|
+
invalid_identities.append(item.identity.address)
|
227
|
+
|
228
|
+
# Remove duplicates
|
229
|
+
invalid_identities = sorted(list(set(invalid_identities)))
|
230
|
+
|
231
|
+
for item in items:
|
232
|
+
if item.identity.address not in invalid_identities:
|
233
|
+
attestations.append(_build_signed_attestation(item))
|
234
|
+
|
235
|
+
if validate_all_before_registration and invalid_identities:
|
236
|
+
return False, invalid_identities
|
237
|
+
|
238
|
+
# get the almanac API endpoint
|
239
|
+
agentverse_config = agentverse_config or AgentverseConfig()
|
240
|
+
almanac_api = urllib.parse.urljoin(agentverse_config.url, DEFAULT_ALMANAC_API_PATH)
|
241
|
+
|
242
|
+
logger.info(
|
243
|
+
msg="Bulk registering with Almanac API",
|
244
|
+
extra={
|
245
|
+
"agent_addresses": [
|
246
|
+
attestation.agent_identifier for attestation in attestations
|
247
|
+
]
|
248
|
+
},
|
249
|
+
)
|
250
|
+
attestation_batch = AgentRegistrationAttestationBatch(
|
251
|
+
attestations=attestations,
|
252
|
+
)
|
253
|
+
|
254
|
+
# submit the attestation to the API
|
255
|
+
status, _ = _send_post_request(
|
256
|
+
url=f"{almanac_api}/agents/batch",
|
257
|
+
data=attestation_batch,
|
258
|
+
timeout=timeout,
|
259
|
+
)
|
260
|
+
return status, invalid_identities
|
261
|
+
|
262
|
+
|
138
263
|
# associate user account with your agent
|
139
264
|
def register_in_agentverse(
|
140
265
|
request: AgentverseConnectRequest,
|
@@ -0,0 +1,82 @@
|
|
1
|
+
"""
|
2
|
+
This module provides methods related to agent bases subscriptions.
|
3
|
+
|
4
|
+
Example usage:
|
5
|
+
```
|
6
|
+
from uagents_core.contrib.protocols.subscriptions import TierType
|
7
|
+
|
8
|
+
@protocol.on_message(ChatMessage)
|
9
|
+
async def handle_message(ctx: Context, sender: str, msg: ChatMessage):
|
10
|
+
subscription_tier = get_subscription_tier(
|
11
|
+
identity=ctx.agent.identity,
|
12
|
+
requester_address=sender
|
13
|
+
)
|
14
|
+
|
15
|
+
if subscription_tier == TierType.PLUS:
|
16
|
+
...
|
17
|
+
|
18
|
+
if subscription_tier == TierType.PRO:
|
19
|
+
...
|
20
|
+
|
21
|
+
...
|
22
|
+
```
|
23
|
+
|
24
|
+
"""
|
25
|
+
|
26
|
+
from datetime import datetime
|
27
|
+
from secrets import token_bytes
|
28
|
+
|
29
|
+
import requests
|
30
|
+
|
31
|
+
from uagents_core.config import AgentverseConfig
|
32
|
+
from uagents_core.contrib.protocols.subscriptions import TierType
|
33
|
+
from uagents_core.identity import Identity
|
34
|
+
from uagents_core.logger import get_logger
|
35
|
+
from uagents_core.storage import compute_attestation
|
36
|
+
|
37
|
+
logger = get_logger("uagents_core.utils.subscriptions")
|
38
|
+
|
39
|
+
|
40
|
+
def get_subscription_tier(
|
41
|
+
identity: Identity,
|
42
|
+
requester_address: str,
|
43
|
+
agentverse_config: AgentverseConfig | None = None,
|
44
|
+
) -> TierType:
|
45
|
+
"""
|
46
|
+
Get the subscription tier of the requester for a specific agent.
|
47
|
+
|
48
|
+
This function is used to verify the type of subscription before processing
|
49
|
+
an incoming message.
|
50
|
+
|
51
|
+
Args:
|
52
|
+
identity (Identity): The identity of the agent that is requested.
|
53
|
+
requester_address (str): The address of the requester to check.
|
54
|
+
agentverse_config (AgentverseConfig | None): The configuration for the Agentverse.
|
55
|
+
If not provided, defaults to a new instance of AgentverseConfig.
|
56
|
+
|
57
|
+
Returns:
|
58
|
+
TierType: The subscription tier type of the requester.
|
59
|
+
"""
|
60
|
+
if not agentverse_config:
|
61
|
+
agentverse_config = AgentverseConfig()
|
62
|
+
attestation: str = compute_attestation(
|
63
|
+
identity=identity,
|
64
|
+
validity_start=datetime.now(),
|
65
|
+
validity_secs=60,
|
66
|
+
nonce=token_bytes(nbytes=32),
|
67
|
+
)
|
68
|
+
url: str = (
|
69
|
+
f"{agentverse_config.payments_endpoint}/subscriptions"
|
70
|
+
+ f"/{identity.address}/{requester_address}"
|
71
|
+
)
|
72
|
+
headers: dict[str, str] = {"Authorization": f"Agent {attestation}"}
|
73
|
+
|
74
|
+
try:
|
75
|
+
response = requests.get(url=url, headers=headers, timeout=10)
|
76
|
+
response.raise_for_status()
|
77
|
+
except requests.RequestException as e:
|
78
|
+
logger.error(f"Failed to get subscription tier: {e}")
|
79
|
+
return TierType.FREE
|
80
|
+
|
81
|
+
data: dict = response.json()
|
82
|
+
return data.get("tier_type", TierType.FREE)
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
{uagents_core-0.3.2 → uagents_core-0.3.4}/uagents_core/contrib/protocols/subscriptions/__init__.py
RENAMED
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|