uagents-core 0.1.3__py3-none-any.whl → 0.2.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.
@@ -1,16 +1,22 @@
1
+ """
2
+ This module provides methods to register your identity with the Fetch.ai services.
3
+ """
4
+
1
5
  import urllib.parse
2
- from typing import List, Optional
3
6
 
4
7
  import requests
8
+ from pydantic import BaseModel
5
9
 
6
10
  from uagents_core.config import (
7
11
  DEFAULT_ALMANAC_API_PATH,
8
12
  DEFAULT_CHALLENGE_PATH,
9
13
  DEFAULT_REGISTRATION_PATH,
14
+ DEFAULT_REQUEST_TIMEOUT,
10
15
  AgentverseConfig,
11
16
  )
12
- from uagents_core.crypto import Identity
17
+ from uagents_core.identity import Identity
13
18
  from uagents_core.logger import get_logger
19
+ from uagents_core.protocol import is_valid_protocol_digest
14
20
  from uagents_core.registration import (
15
21
  AgentRegistrationAttestation,
16
22
  AgentUpdates,
@@ -25,22 +31,77 @@ from uagents_core.types import AgentEndpoint
25
31
  logger = get_logger("uagents_core.utils.registration")
26
32
 
27
33
 
34
+ def _send_post_request(
35
+ url: str,
36
+ data: BaseModel,
37
+ *,
38
+ headers: dict[str, str] | None = None,
39
+ timeout: int = DEFAULT_REQUEST_TIMEOUT,
40
+ ) -> tuple[bool, requests.Response | None]:
41
+ final_headers: dict[str, str] = {"content-type": "application/json"}
42
+ if headers:
43
+ final_headers.update(headers)
44
+ try:
45
+ response: requests.Response = requests.post(
46
+ url=url,
47
+ headers=final_headers,
48
+ data=data.model_dump_json(),
49
+ timeout=timeout,
50
+ )
51
+ response.raise_for_status()
52
+ return True, response
53
+ except requests.RequestException as e:
54
+ logger.error(
55
+ msg="Error submitting request",
56
+ extra={"url": url, "data": data.model_dump_json()},
57
+ exc_info=e,
58
+ )
59
+ return False, None
60
+
61
+
28
62
  def register_in_almanac(
29
- request: AgentverseConnectRequest,
30
63
  identity: Identity,
64
+ endpoints: list[str],
65
+ protocol_digests: list[str],
31
66
  *,
32
- protocol_digests: List[str],
33
- agentverse_config: Optional[AgentverseConfig] = None,
34
- ):
67
+ agentverse_config: AgentverseConfig | None = None,
68
+ timeout: int = DEFAULT_REQUEST_TIMEOUT,
69
+ ) -> bool:
35
70
  """
36
- Register the agent with the Almanac API.
71
+ Register the identity with the Almanac API to make it discoverable by other agents.
37
72
 
38
73
  Args:
39
- request (AgentverseConnectRequest): The request containing the agent details.
40
74
  identity (Identity): The identity of the agent.
41
- protocol_digest (List[str]): The digest of the protocol that the agent supports
75
+ endpoints (list[str]): The endpoints that the agent can be reached at.
76
+ protocol_digests (list[str]): The digests of the protocol that the agent supports
42
77
  agentverse_config (AgentverseConfig): The configuration for the agentverse API
78
+ timeout (int): The timeout for the request
43
79
  """
80
+ # check endpoints
81
+ if not endpoints:
82
+ logger.warning("No endpoints provided; skipping registration")
83
+ return False
84
+ for endpoint in endpoints:
85
+ result = urllib.parse.urlparse(endpoint)
86
+ if not all([result.scheme, result.netloc]):
87
+ logger.error(
88
+ msg="Invalid endpoint provided; skipping registration",
89
+ extra={"endpoint": endpoint},
90
+ )
91
+ return False
92
+
93
+ agent_endpoints: list[AgentEndpoint] = [
94
+ AgentEndpoint(url=endpoint, weight=1) for endpoint in endpoints
95
+ ]
96
+
97
+ # check protocol digests
98
+ for proto_digest in protocol_digests:
99
+ if not is_valid_protocol_digest(proto_digest):
100
+ logger.error(
101
+ msg="Invalid protocol digest provided; skipping registration",
102
+ extra={"protocol_digest": proto_digest},
103
+ )
104
+ return False
44
105
 
45
106
  # get the almanac API endpoint
46
107
  agentverse_config = agentverse_config or AgentverseConfig()
@@ -49,83 +110,51 @@ def register_in_almanac(
49
110
  # get the agent address
50
111
  agent_address = identity.address
51
112
 
52
- registration_metadata = {
53
- "almanac_endpoint": almanac_api,
54
- "agent_address": agent_address,
55
- "agent_endpoint": request.endpoint or "",
56
- "protocol_digest": ",".join(protocol_digests),
57
- }
58
- if request.endpoint is None:
59
- if request.agent_type == "mailbox":
60
- request.endpoint = f"{agentverse_config.url}/v1/submit"
61
- elif request.agent_type == "proxy":
62
- request.endpoint = f"{agentverse_config.url}/v1/proxy/submit"
63
-
64
- if request.endpoint is None:
65
- logger.warning(
66
- "No endpoint provided for agent registration",
67
- extra=registration_metadata,
68
- )
69
- return
70
-
71
- logger.info(
72
- "Registering with Almanac API",
73
- extra=registration_metadata,
74
- )
75
-
76
113
  # create the attestation
77
114
  attestation = AgentRegistrationAttestation(
78
115
  agent_identifier=agent_address,
79
116
  protocols=protocol_digests,
80
- endpoints=[
81
- AgentEndpoint(url=request.endpoint, weight=1),
82
- ],
117
+ endpoints=agent_endpoints,
83
118
  metadata=None,
84
119
  )
85
120
 
121
+ logger.info(msg="Registering with Almanac API", extra=attestation.model_dump())
122
+
86
123
  # sign the attestation
87
124
  attestation.sign(identity)
88
125
 
89
126
  # submit the attestation to the API
90
- r = requests.post(
91
- f"{almanac_api}/agents",
92
- headers={"content-type": "application/json"},
93
- data=attestation.model_dump_json(),
94
- )
95
- r.raise_for_status()
96
- logger.debug(
97
- "Agent attestation submitted",
98
- extra=registration_metadata,
127
+ status, _ = _send_post_request(
128
+ url=f"{almanac_api}/agents", data=attestation, timeout=timeout
99
129
  )
130
+ return status
100
131
 
101
132
 
133
+ # associate user account with your agent
102
134
  def register_in_agentverse(
103
135
  request: AgentverseConnectRequest,
104
136
  identity: Identity,
105
- agent_details: Optional[AgentUpdates] = None,
106
137
  *,
107
- agentverse_config: Optional[AgentverseConfig] = None,
108
- ):
138
+ agent_details: AgentUpdates | None = None,
139
+ agentverse_config: AgentverseConfig | None = None,
140
+ timeout: int = DEFAULT_REQUEST_TIMEOUT,
141
+ ) -> bool:
109
142
  """
110
- Register the agent with the Agentverse API.
143
+ Register an agent in Agentverse and update its details if provided.
111
144
 
112
145
  Args:
113
146
  request (AgentverseConnectRequest): The request containing the agent details.
114
147
  identity (Identity): The identity of the agent.
115
- agent_details (Optional[AgentUpdates]): The agent details to update.
116
- agentverse_config (AgentverseConfig): The configuration for the agentverse API
117
- Returns:
118
- None
148
+ agent_details (AgentUpdates | None): The agent details to update.
149
+ agentverse_config (AgentverseConfig | None): The configuration for the agentverse API
150
+ timeout (int): The timeout for the requests
119
151
  """
120
-
121
- # API endpoints
122
152
  agentverse_config = agentverse_config or AgentverseConfig()
123
153
  registration_api = urllib.parse.urljoin(
124
154
  agentverse_config.url, DEFAULT_REGISTRATION_PATH
125
155
  )
126
156
  challenge_api = urllib.parse.urljoin(agentverse_config.url, DEFAULT_CHALLENGE_PATH)
127
157
 
128
- # get the agent address
129
158
  agent_address = identity.address
130
159
 
131
160
  registration_metadata = {
@@ -138,87 +167,106 @@ def register_in_agentverse(
138
167
  }
139
168
 
140
169
  # check to see if the agent exists
141
- r = requests.get(
170
+ response = requests.get(
142
171
  f"{registration_api}/{agent_address}",
143
172
  headers={
144
173
  "content-type": "application/json",
145
174
  "authorization": f"Bearer {request.user_token}",
146
175
  },
176
+ timeout=timeout,
147
177
  )
148
178
 
149
179
  # if it doesn't then create it
150
- if r.status_code == 404:
180
+ if response.status_code == 404:
151
181
  logger.debug(
152
- "Agent did not exist on agentverse; registering it",
182
+ msg="Agent does not exist on agentverse; registering it...",
153
183
  extra=registration_metadata,
154
184
  )
155
185
 
156
186
  challenge_request = ChallengeRequest(address=identity.address)
157
187
  logger.debug(
158
- "Requesting mailbox access challenge",
159
- extra=registration_metadata,
188
+ msg="Requesting mailbox access challenge", extra=registration_metadata
160
189
  )
161
- r = requests.post(
162
- challenge_api,
163
- data=challenge_request.model_dump_json(),
164
- headers={
165
- "content-type": "application/json",
166
- "Authorization": f"Bearer {request.user_token}",
167
- },
190
+ status, response = _send_post_request(
191
+ url=challenge_api,
192
+ data=challenge_request,
193
+ headers={"authorization": f"Bearer {request.user_token}"},
194
+ timeout=timeout,
168
195
  )
169
- r.raise_for_status()
170
- challenge = ChallengeResponse.model_validate_json(r.text)
196
+ if not status or not response:
197
+ logger.error(
198
+ msg="Error requesting mailbox access challenge",
199
+ extra=registration_metadata,
200
+ )
201
+ return False
202
+
203
+ challenge = ChallengeResponse.model_validate_json(response.text)
171
204
  registration_payload = RegistrationRequest(
172
205
  address=identity.address,
173
206
  challenge=challenge.challenge,
174
207
  challenge_response=identity.sign(challenge.challenge.encode()),
175
208
  endpoint=request.endpoint,
176
209
  agent_type=request.agent_type,
177
- ).model_dump_json()
178
- r = requests.post(
179
- registration_api,
180
- headers={
181
- "content-type": "application/json",
182
- "authorization": f"Bearer {request.user_token}",
183
- },
210
+ )
211
+ status, response = _send_post_request(
212
+ url=registration_api,
184
213
  data=registration_payload,
214
+ headers={"authorization": f"Bearer {request.user_token}"},
215
+ timeout=timeout,
185
216
  )
186
- if r.status_code == 409:
217
+ if not status or not response:
218
+ logger.error(
219
+ msg="Error registering agent with Agentverse",
220
+ extra=registration_metadata,
221
+ )
222
+ return False
223
+ if response.status_code == 409:
187
224
  logger.info(
188
- "Agent already registered with Agentverse",
225
+ msg="Agent already registered with Agentverse",
189
226
  extra=registration_metadata,
190
227
  )
191
228
  else:
192
- r.raise_for_status()
193
- registration_response = RegistrationResponse.model_validate_json(r.text)
229
+ registration_response = RegistrationResponse.model_validate_json(
230
+ response.text
231
+ )
194
232
  if registration_response.success:
195
233
  logger.info(
196
- f"Successfully registered as {request.agent_type} agent in Agentverse",
234
+ msg=f"Successfully registered as {request.agent_type} agent in Agentverse",
197
235
  extra=registration_metadata,
198
236
  )
237
+
199
238
  if not agent_details:
200
239
  logger.debug(
201
- "No agent details provided; skipping agent update",
240
+ msg="No agent details provided; skipping agent update",
202
241
  extra=registration_metadata,
203
242
  )
204
- return
243
+ return True
205
244
 
206
- # update the readme and the title of the agent to make it easier to find
245
+ # update the readme and the name of the agent to make it easier to find
207
246
  logger.debug(
208
- "Registering agent title and readme with Agentverse",
209
- extra=registration_metadata,
210
- )
211
- update = AgentUpdates(name=agent_details.name, readme=agent_details.readme)
212
- r = requests.put(
213
- f"{registration_api}/{agent_address}",
214
- headers={
215
- "content-type": "application/json",
216
- "authorization": f"Bearer {request.user_token}",
217
- },
218
- data=update.model_dump_json(),
219
- )
220
- r.raise_for_status()
221
- logger.info(
222
- "Completed registering agent with Agentverse",
247
+ msg="Registering agent details with Agentverse",
223
248
  extra=registration_metadata,
224
249
  )
250
+ try:
251
+ response = requests.put(
252
+ url=f"{registration_api}/{agent_address}",
253
+ headers={
254
+ "content-type": "application/json",
255
+ "authorization": f"Bearer {request.user_token}",
256
+ },
257
+ data=agent_details.model_dump_json(),
258
+ timeout=timeout,
259
+ )
260
+ response.raise_for_status()
261
+ logger.info(
262
+ msg="Completed registering agent with Agentverse",
263
+ extra=registration_metadata,
264
+ )
265
+ return True
266
+ except requests.RequestException as e:
267
+ logger.error(
268
+ msg="Error registering agent with Agentverse",
269
+ extra=registration_metadata,
270
+ exc_info=e,
271
+ )
272
+ return False
@@ -0,0 +1,73 @@
1
+ """This module provides methods to resolve an agent address."""
2
+
3
+ import urllib.parse
4
+ from typing import Any
5
+
6
+ import requests
7
+
8
+ from uagents_core.config import (
9
+ DEFAULT_ALMANAC_API_PATH,
10
+ DEFAULT_MAX_ENDPOINTS,
11
+ DEFAULT_REQUEST_TIMEOUT,
12
+ AgentverseConfig,
13
+ )
14
+ from uagents_core.helpers import weighted_random_sample
15
+ from uagents_core.identity import parse_identifier
16
+ from uagents_core.logger import get_logger
17
+
18
+ logger = get_logger("uagents_core.utils.resolver")
19
+
20
+
21
+ def lookup_endpoint_for_agent(
22
+ agent_identifier: str,
23
+ *,
24
+ max_endpoints: int = DEFAULT_MAX_ENDPOINTS,
25
+ agentverse_config: AgentverseConfig | None = None,
26
+ ) -> list[str]:
27
+ """
28
+ Resolve the endpoints for an agent using the Almanac API.
29
+
30
+ Args:
31
+ destination (str): The destination address to look up.
32
+
33
+ Returns:
34
+ List[str]: The endpoint(s) for the agent.
35
+ """
36
+ _, _, agent_address = parse_identifier(agent_identifier)
37
+
38
+ agentverse_config = agentverse_config or AgentverseConfig()
39
+ almanac_api = urllib.parse.urljoin(agentverse_config.url, DEFAULT_ALMANAC_API_PATH)
40
+
41
+ request_meta: dict[str, Any] = {
42
+ "agent_address": agent_address,
43
+ "lookup_url": almanac_api,
44
+ }
45
+ logger.debug(msg="looking up endpoint for agent", extra=request_meta)
46
+ try:
47
+ response = requests.get(
48
+ url=f"{almanac_api}/agents/{agent_address}", timeout=DEFAULT_REQUEST_TIMEOUT
49
+ )
50
+ response.raise_for_status()
51
+ except requests.RequestException as e:
52
+ request_meta["exception"] = e
53
+ logger.error(msg="Error looking up agent endpoint", extra=request_meta)
54
+ return []
55
+
56
+ request_meta["response_status"] = response.status_code
57
+ logger.info(
58
+ msg="Got response looking up agent endpoint",
59
+ extra=request_meta,
60
+ )
61
+
62
+ endpoints: list = response.json().get("endpoints", [])
63
+
64
+ if len(endpoints) > 0:
65
+ urls = [val.get("url") for val in endpoints]
66
+ weights = [val.get("weight") for val in endpoints]
67
+ return weighted_random_sample(
68
+ items=urls,
69
+ weights=weights,
70
+ k=min(max_endpoints, len(endpoints)),
71
+ )
72
+
73
+ return []
@@ -1,27 +1,27 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: uagents-core
3
- Version: 0.1.3
3
+ Version: 0.2.0
4
4
  Summary: Core components for agent based systems
5
5
  License: Apache 2.0
6
6
  Author: Ed FitzGerald
7
7
  Author-email: edward.fitzgerald@fetch.ai
8
- Requires-Python: >=3.9,<3.13
8
+ Requires-Python: >=3.10,<4.0
9
9
  Classifier: License :: Other/Proprietary License
10
10
  Classifier: Programming Language :: Python :: 3
11
- Classifier: Programming Language :: Python :: 3.9
12
11
  Classifier: Programming Language :: Python :: 3.10
13
12
  Classifier: Programming Language :: Python :: 3.11
14
13
  Classifier: Programming Language :: Python :: 3.12
15
- Requires-Dist: aiohttp (>=3.8.3,<4.0.0)
16
- Requires-Dist: bech32 (>=1.2.0,<2.0.0)
17
- Requires-Dist: ecdsa (>=0.19.0,<0.20.0)
18
- Requires-Dist: msgpack (>=1.0.4,<2.0.0)
14
+ Classifier: Programming Language :: Python :: 3.13
15
+ Requires-Dist: bech32 (>=1.2.0,<2.0)
16
+ Requires-Dist: ecdsa (>=0.19.0,<1.0)
19
17
  Requires-Dist: pydantic (>=2.8,<3.0)
20
18
  Requires-Dist: requests (>=2.32.3,<3.0)
21
- Requires-Dist: rich (>=13.9.4,<14.0.0)
22
- Requires-Dist: structlog (>=24.4.0,<25.0.0)
19
+ Project-URL: Documentation, https://fetch.ai/docs
20
+ Project-URL: Homepage, https://fetch.ai
21
+ Project-URL: Repository, https://github.com/fetchai/uAgents
23
22
  Description-Content-Type: text/markdown
24
23
 
25
- # UAgents-Core
24
+ # uAgents-Core
26
25
 
27
26
  Core definitions and functionalities to build agent which can interact and integrate with Fetch.ai ecosystem and agent marketplace.
27
+
@@ -0,0 +1,19 @@
1
+ uagents_core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ uagents_core/config.py,sha256=mvCXtvt_efDbmoTcw0JNHymWZGOtXxIYXlrooWfbY1Q,798
3
+ uagents_core/contrib/protocols/chat/__init__.py,sha256=EAt2uQeaZtrSEdt2SiFyqFcC4nxKtyoJRaHhjKtwngw,3032
4
+ uagents_core/contrib/protocols/subscriptions/__init__.py,sha256=HsxWzLOvP83a7RkHRx8y_uXTJzJtuuBcyRNj33TT7xA,1944
5
+ uagents_core/envelope.py,sha256=eGq-Te-z2nRQVJIPqRdf-h48hGwg7IOKptIQZKYKqFU,3527
6
+ uagents_core/helpers.py,sha256=Nfl3V5Vl_23O4Ewtwlsh0v5rxthkZPm6b4BvsLGDKn8,943
7
+ uagents_core/identity.py,sha256=YHI3rromJ27DGF3X102MGkuvBIZ6usAaMutlDLdVaL0,6185
8
+ uagents_core/logger.py,sha256=5XLs3-XpKDeoSwtAE5pEM9h6x3pUPNbb0T0J7KvFmG0,877
9
+ uagents_core/models.py,sha256=fxsFjRochkJUdgPltT1HWgBl-9K-HVQWl8sSMVgeJFY,1066
10
+ uagents_core/protocol.py,sha256=T9jasOkltne33E16Y7VrqcB2moWVsv-Qh4XLZotyz8g,5428
11
+ uagents_core/registration.py,sha256=h7ha58H7XbLQ__qOHMKVnHShzMlWfd2xrK9fRdgfb9w,2953
12
+ uagents_core/types.py,sha256=_W3EN1wEIRFxuhhBxyZxQH_dA_3AtoPPReIzrgcTUHc,1167
13
+ uagents_core/utils/__init__.py,sha256=v0MaxDYCTtQlwbblEHCfLtbeTnA2hCmKKJk7mlcE20U,135
14
+ uagents_core/utils/messages.py,sha256=O4Fk8KULi523ruynEw1ZxAxFKi4ex816R4a_Q1E24O8,5056
15
+ uagents_core/utils/registration.py,sha256=ny_2Svz1RLgMcH0WgBlzWKhZnhN25Yuni2eHFksoMjU,9039
16
+ uagents_core/utils/resolver.py,sha256=X18oe-WPU_6pfTc6x_Oa69kZQYCMidkaSeCs68Poaik,2169
17
+ uagents_core-0.2.0.dist-info/METADATA,sha256=OAsIUaGEiH9rehxRlGedKly53jQk1-GjEqA7Q-cwud0,1009
18
+ uagents_core-0.2.0.dist-info/WHEEL,sha256=XbeZDeTWKc1w7CSIyre5aMDU_-PohRwTQceYnisIYYY,88
19
+ uagents_core-0.2.0.dist-info/RECORD,,
@@ -1,76 +0,0 @@
1
- import random
2
- from typing import Any, List, Optional, Tuple
3
-
4
- from uagents_core.config import (
5
- AGENT_ADDRESS_LENGTH,
6
- AGENT_PREFIX,
7
- )
8
- from uagents_core.crypto import is_user_address
9
-
10
-
11
- def weighted_random_sample(
12
- items: List[Any], weights: Optional[List[float]] = None, k: int = 1, rng=random
13
- ) -> List[Any]:
14
- """
15
- Weighted random sample from a list of items without replacement.
16
-
17
- Ref: Efraimidis, Pavlos S. "Weighted random sampling over data streams."
18
-
19
- Args:
20
- items (List[Any]): The list of items to sample from.
21
- weights (Optional[List[float]]): The optional list of weights for each item.
22
- k (int): The number of items to sample.
23
- rng (random): The random number generator.
24
-
25
- Returns:
26
- List[Any]: The sampled items.
27
- """
28
- if weights is None:
29
- return rng.sample(items, k=k)
30
- values = [rng.random() ** (1 / w) for w in weights]
31
- order = sorted(range(len(items)), key=lambda i: values[i])
32
- return [items[i] for i in order[-k:]]
33
-
34
-
35
- def is_valid_address(address: str) -> bool:
36
- """
37
- Check if the given string is a valid address.
38
-
39
- Args:
40
- address (str): The address to be checked.
41
-
42
- Returns:
43
- bool: True if the address is valid; False otherwise.
44
- """
45
- return is_user_address(address) or (
46
- len(address) == AGENT_ADDRESS_LENGTH and address.startswith(AGENT_PREFIX)
47
- )
48
-
49
-
50
- def parse_identifier(identifier: str) -> Tuple[str, str, str]:
51
- """
52
- Parse an agent identifier string into prefix, name, and address.
53
-
54
- Args:
55
- identifier (str): The identifier string to be parsed.
56
-
57
- Returns:
58
- Tuple[str, str, str]: A Tuple containing the prefix, name, and address as strings.
59
- """
60
-
61
- prefix = ""
62
- name = ""
63
- address = ""
64
-
65
- if "://" in identifier:
66
- prefix, identifier = identifier.split("://", 1)
67
-
68
- if "/" in identifier:
69
- name, identifier = identifier.split("/", 1)
70
-
71
- if is_valid_address(identifier):
72
- address = identifier
73
- else:
74
- name = identifier
75
-
76
- return prefix, name, address