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.
Files changed (23) hide show
  1. {uagents_core-0.3.2 → uagents_core-0.3.4}/PKG-INFO +1 -1
  2. {uagents_core-0.3.2 → uagents_core-0.3.4}/pyproject.toml +1 -1
  3. {uagents_core-0.3.2 → uagents_core-0.3.4}/uagents_core/config.py +5 -0
  4. {uagents_core-0.3.2 → uagents_core-0.3.4}/uagents_core/registration.py +4 -0
  5. {uagents_core-0.3.2 → uagents_core-0.3.4}/uagents_core/storage.py +47 -16
  6. {uagents_core-0.3.2 → uagents_core-0.3.4}/uagents_core/utils/registration.py +140 -15
  7. uagents_core-0.3.4/uagents_core/utils/subscriptions.py +82 -0
  8. {uagents_core-0.3.2 → uagents_core-0.3.4}/README.md +0 -0
  9. {uagents_core-0.3.2 → uagents_core-0.3.4}/uagents_core/__init__.py +0 -0
  10. {uagents_core-0.3.2 → uagents_core-0.3.4}/uagents_core/contrib/__init__.py +0 -0
  11. {uagents_core-0.3.2 → uagents_core-0.3.4}/uagents_core/contrib/protocols/__init__.py +0 -0
  12. {uagents_core-0.3.2 → uagents_core-0.3.4}/uagents_core/contrib/protocols/chat/__init__.py +0 -0
  13. {uagents_core-0.3.2 → uagents_core-0.3.4}/uagents_core/contrib/protocols/subscriptions/__init__.py +0 -0
  14. {uagents_core-0.3.2 → uagents_core-0.3.4}/uagents_core/envelope.py +0 -0
  15. {uagents_core-0.3.2 → uagents_core-0.3.4}/uagents_core/helpers.py +0 -0
  16. {uagents_core-0.3.2 → uagents_core-0.3.4}/uagents_core/identity.py +0 -0
  17. {uagents_core-0.3.2 → uagents_core-0.3.4}/uagents_core/logger.py +0 -0
  18. {uagents_core-0.3.2 → uagents_core-0.3.4}/uagents_core/models.py +0 -0
  19. {uagents_core-0.3.2 → uagents_core-0.3.4}/uagents_core/protocol.py +0 -0
  20. {uagents_core-0.3.2 → uagents_core-0.3.4}/uagents_core/types.py +0 -0
  21. {uagents_core-0.3.2 → uagents_core-0.3.4}/uagents_core/utils/__init__.py +0 -0
  22. {uagents_core-0.3.2 → uagents_core-0.3.4}/uagents_core/utils/messages.py +0 -0
  23. {uagents_core-0.3.2 → uagents_core-0.3.4}/uagents_core/utils/resolver.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: uagents-core
3
- Version: 0.3.2
3
+ Version: 0.3.4
4
4
  Summary: Core components for agent based systems
5
5
  License: Apache 2.0
6
6
  Author: Ed FitzGerald
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "uagents-core"
3
- version = "0.3.2"
3
+ version = "0.3.4"
4
4
  description = "Core components for agent based systems"
5
5
  authors = [
6
6
  { name = "Ed FitzGerald", email = "edward.fitzgerald@fetch.ai" },
@@ -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
- identity: Optional[Identity] = None,
38
- storage_url: Optional[str] = None,
39
- api_token: Optional[str] = None,
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
- return compute_attestation(self.identity, now, 3600, nonce)
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
- elif self.identity:
64
+ if self.identity:
54
65
  return {"Authorization": f"Agent {self._make_attestation()}"}
55
- else:
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, asset_id: str, content: bytes, mime_type: str = "text/plain"
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(content).decode(),
78
+ "contents": base64.b64encode(asset_content).decode(),
66
79
  "mime_type": mime_type,
67
80
  }
68
- response = requests.put(url, json=payload, headers=headers)
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
- response = requests.get(url, headers=headers)
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(url, json=payload, headers=headers)
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(url, json=payload, headers=headers)
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
- attestation = AgentRegistrationAttestation(
120
- agent_identifier=agent_address,
121
- protocols=protocol_digests,
122
- endpoints=agent_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