uagents-core 0.1.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.
File without changes
@@ -0,0 +1,76 @@
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
uagents_core/config.py ADDED
@@ -0,0 +1,21 @@
1
+ from pydantic import BaseModel
2
+
3
+ DEFAULT_AGENTVERSE_URL = "agentverse.ai"
4
+ DEFAULT_ALMANAC_API_PATH = "/v1/almanac"
5
+ DEFAULT_REGISTRATION_PATH = "/v1/agents"
6
+ DEFAULT_CHALLENGE_PATH = "/v1/auth/challenge"
7
+
8
+ DEFAULT_MAX_ENDPOINTS = 10
9
+
10
+ AGENT_ADDRESS_LENGTH = 65
11
+ AGENT_PREFIX = "agent"
12
+
13
+
14
+ class AgentverseConfig(BaseModel):
15
+ base_url: str = DEFAULT_AGENTVERSE_URL
16
+ protocol: str = "https"
17
+ http_prefix: str = "https"
18
+
19
+ @property
20
+ def url(self) -> str:
21
+ return f"{self.http_prefix}://{self.base_url}"
uagents_core/crypto.py ADDED
@@ -0,0 +1,157 @@
1
+ import base64
2
+ import hashlib
3
+ import struct
4
+ from secrets import token_bytes
5
+ from typing import Tuple, Union
6
+
7
+ import bech32
8
+ import ecdsa
9
+ from ecdsa.util import sigencode_string_canonize
10
+
11
+ USER_PREFIX = "user"
12
+ SHA_LENGTH = 256
13
+
14
+
15
+ def _decode_bech32(value: str) -> Tuple[str, bytes]:
16
+ prefix, data_base5 = bech32.bech32_decode(value)
17
+ data = bytes(bech32.convertbits(data_base5, 5, 8, False))
18
+ return prefix, data
19
+
20
+
21
+ def _encode_bech32(prefix: str, value: bytes) -> str:
22
+ value_base5 = bech32.convertbits(value, 8, 5)
23
+ return bech32.bech32_encode(prefix, value_base5)
24
+
25
+
26
+ def is_user_address(address: str) -> bool:
27
+ return address[0 : len(USER_PREFIX)] == USER_PREFIX
28
+
29
+
30
+ def generate_user_address() -> str:
31
+ return _encode_bech32(USER_PREFIX, token_bytes(32))
32
+
33
+
34
+ def _key_derivation_hash(prefix: str, index: int) -> bytes:
35
+ hasher = hashlib.sha256()
36
+ hasher.update(prefix.encode())
37
+ assert 0 <= index < SHA_LENGTH
38
+ hasher.update(bytes([index]))
39
+ return hasher.digest()
40
+
41
+
42
+ def _seed_hash(seed: str) -> bytes:
43
+ hasher = hashlib.sha256()
44
+ hasher.update(seed.encode())
45
+ return hasher.digest()
46
+
47
+
48
+ def derive_key_from_seed(seed, prefix, index) -> bytes:
49
+ hasher = hashlib.sha256()
50
+ hasher.update(_key_derivation_hash(prefix, index))
51
+ hasher.update(_seed_hash(seed))
52
+ return hasher.digest()
53
+
54
+
55
+ def encode_length_prefixed(value: Union[str, int, bytes]) -> bytes:
56
+ if isinstance(value, str):
57
+ encoded = value.encode()
58
+ elif isinstance(value, int):
59
+ encoded = struct.pack(">Q", value)
60
+ elif isinstance(value, bytes):
61
+ encoded = value
62
+ else:
63
+ raise AssertionError()
64
+
65
+ length = len(encoded)
66
+ prefix = struct.pack(">Q", length)
67
+
68
+ return prefix + encoded
69
+
70
+
71
+ class Identity:
72
+ """An identity is a cryptographic keypair that can be used to sign messages."""
73
+
74
+ def __init__(self, signing_key: ecdsa.SigningKey):
75
+ """Create a new identity from a signing key."""
76
+ self._sk = signing_key
77
+
78
+ # build the address
79
+ pub_key_bytes = self._sk.get_verifying_key().to_string(encoding="compressed")
80
+ self._address = _encode_bech32("agent", pub_key_bytes)
81
+ self._pub_key = pub_key_bytes.hex()
82
+
83
+ @staticmethod
84
+ def from_seed(seed: str, index: int) -> "Identity":
85
+ """Create a new identity from a seed and index."""
86
+ key = derive_key_from_seed(seed, "agent", index)
87
+ signing_key = ecdsa.SigningKey.from_string(
88
+ key,
89
+ curve=ecdsa.SECP256k1,
90
+ hashfunc=hashlib.sha256,
91
+ )
92
+ return Identity(signing_key)
93
+
94
+ @staticmethod
95
+ def generate() -> "Identity":
96
+ """Generate a random new identity."""
97
+ signing_key = ecdsa.SigningKey.generate(
98
+ curve=ecdsa.SECP256k1,
99
+ hashfunc=hashlib.sha256,
100
+ )
101
+ return Identity(signing_key)
102
+
103
+ @staticmethod
104
+ def from_string(private_key_hex: str) -> "Identity":
105
+ """Create a new identity from a private key."""
106
+ bytes_key = bytes.fromhex(private_key_hex)
107
+ signing_key = ecdsa.SigningKey.from_string(
108
+ bytes_key,
109
+ curve=ecdsa.SECP256k1,
110
+ hashfunc=hashlib.sha256,
111
+ )
112
+
113
+ return Identity(signing_key)
114
+
115
+ # this is not the real private key but a signing key derived from the private key
116
+ @property
117
+ def private_key(self) -> str:
118
+ """Property to access the private key of the identity."""
119
+ return self._sk.to_string().hex()
120
+
121
+ @property
122
+ def address(self) -> str:
123
+ """Property to access the address of the identity."""
124
+ return self._address
125
+
126
+ @property
127
+ def pub_key(self) -> str:
128
+ return self._pub_key
129
+
130
+ def sign(self, data: bytes) -> str:
131
+ """Sign the provided data."""
132
+ return _encode_bech32("sig", self._sk.sign(data))
133
+
134
+ def sign_b64(self, data: bytes) -> str:
135
+ raw_signature = bytes(self._sk.sign(data, sigencode=sigencode_string_canonize))
136
+ return base64.b64encode(raw_signature).decode()
137
+
138
+ def sign_digest(self, digest: bytes) -> str:
139
+ """Sign the provided digest."""
140
+ return _encode_bech32("sig", self._sk.sign_digest(digest))
141
+
142
+ @staticmethod
143
+ def verify_digest(address: str, digest: bytes, signature: str) -> bool:
144
+ """Verify that the signature is correct for the provided signer address and digest."""
145
+ pk_prefix, pk_data = _decode_bech32(address)
146
+ sig_prefix, sig_data = _decode_bech32(signature)
147
+
148
+ if pk_prefix != "agent":
149
+ raise ValueError("Unable to decode agent address")
150
+
151
+ if sig_prefix != "sig":
152
+ raise ValueError("Unable to decode signature")
153
+
154
+ # build the verifying key
155
+ verifying_key = ecdsa.VerifyingKey.from_string(pk_data, curve=ecdsa.SECP256k1)
156
+
157
+ return verifying_key.verify_digest(sig_data, digest)
@@ -0,0 +1,163 @@
1
+ """Agent Envelope."""
2
+
3
+ import base64
4
+ import hashlib
5
+ import struct
6
+ import time
7
+ from typing import List, Optional
8
+
9
+ from pydantic import (
10
+ UUID4,
11
+ BaseModel,
12
+ Field,
13
+ field_serializer,
14
+ )
15
+
16
+ from uagents_core.crypto import Identity
17
+ from uagents_core.types import JsonStr
18
+
19
+
20
+ class Envelope(BaseModel):
21
+ """
22
+ Represents an envelope for message communication between agents.
23
+
24
+ Attributes:
25
+ version (int): The envelope version.
26
+ sender (str): The sender's address.
27
+ target (str): The target's address.
28
+ session (UUID4): The session UUID that persists for back-and-forth
29
+ dialogues between agents.
30
+ schema_digest (str): The schema digest for the enclosed message.
31
+ protocol_digest (Optional[str]): The digest of the protocol associated with the message
32
+ (optional).
33
+ payload (Optional[str]): The encoded message payload of the envelope (optional).
34
+ expires (Optional[int]): The expiration timestamp (optional).
35
+ nonce (Optional[int]): The nonce value (optional).
36
+ signature (Optional[str]): The envelope signature (optional).
37
+ """
38
+
39
+ version: int
40
+ sender: str
41
+ target: str
42
+ session: UUID4
43
+ schema_digest: str
44
+ protocol_digest: Optional[str] = None
45
+ payload: Optional[str] = None
46
+ expires: Optional[int] = None
47
+ nonce: Optional[int] = None
48
+ signature: Optional[str] = None
49
+
50
+ def encode_payload(self, value: JsonStr):
51
+ """
52
+ Encode the payload value and store it in the envelope.
53
+
54
+ Args:
55
+ value (JsonStr): The payload value to be encoded.
56
+ """
57
+ self.payload = base64.b64encode(value.encode()).decode()
58
+
59
+ def decode_payload(self) -> str:
60
+ """
61
+ Decode and retrieve the payload value from the envelope.
62
+
63
+ Returns:
64
+ str: The decoded payload value, or '' if payload is not present.
65
+ """
66
+ if self.payload is None:
67
+ return ""
68
+
69
+ return base64.b64decode(self.payload).decode()
70
+
71
+ def sign(self, identity: Identity):
72
+ """
73
+ Sign the envelope with the provided identity.
74
+
75
+ Args:
76
+ identity (Identity): The identity to use for signing.
77
+
78
+ Raises:
79
+ ValueError: If the signature cannot be computed.
80
+ """
81
+ try:
82
+ self.signature = identity.sign_digest(self._digest())
83
+ except Exception as err:
84
+ raise ValueError(f"Failed to sign envelope: {err}") from err
85
+
86
+ def verify(self) -> bool:
87
+ """
88
+ Verify the envelope's signature.
89
+
90
+ Returns:
91
+ bool: True if the signature is valid.
92
+
93
+ Raises:
94
+ ValueError: If the signature is missing.
95
+ ecdsa.BadSignatureError: If the signature is invalid.
96
+ """
97
+ if self.signature is None:
98
+ raise ValueError("Envelope signature is missing")
99
+ return Identity.verify_digest(self.sender, self._digest(), self.signature)
100
+
101
+ def _digest(self) -> bytes:
102
+ """
103
+ Compute the digest of the envelope's content.
104
+
105
+ Returns:
106
+ bytes: The computed digest.
107
+ """
108
+ hasher = hashlib.sha256()
109
+ hasher.update(self.sender.encode())
110
+ hasher.update(self.target.encode())
111
+ hasher.update(str(self.session).encode())
112
+ hasher.update(self.schema_digest.encode())
113
+ if self.payload is not None:
114
+ hasher.update(self.payload.encode())
115
+ if self.expires is not None:
116
+ hasher.update(struct.pack(">Q", self.expires))
117
+ if self.nonce is not None:
118
+ hasher.update(struct.pack(">Q", self.nonce))
119
+ return hasher.digest()
120
+
121
+
122
+ class EnvelopeHistoryEntry(BaseModel):
123
+ timestamp: int = Field(default_factory=lambda: int(time.time()))
124
+ version: int
125
+ sender: str
126
+ target: str
127
+ session: UUID4
128
+ schema_digest: str
129
+ protocol_digest: Optional[str] = None
130
+ payload: Optional[str] = None
131
+
132
+ @field_serializer("session")
133
+ def serialize_session(self, session: UUID4, _info):
134
+ return str(session)
135
+
136
+ @classmethod
137
+ def from_envelope(cls, envelope: Envelope):
138
+ return cls(
139
+ version=envelope.version,
140
+ sender=envelope.sender,
141
+ target=envelope.target,
142
+ session=envelope.session,
143
+ schema_digest=envelope.schema_digest,
144
+ protocol_digest=envelope.protocol_digest,
145
+ payload=envelope.decode_payload(),
146
+ )
147
+
148
+
149
+ class EnvelopeHistory(BaseModel):
150
+ envelopes: List[EnvelopeHistoryEntry]
151
+
152
+ def add_entry(self, entry: EnvelopeHistoryEntry):
153
+ self.envelopes.append(entry)
154
+ self.apply_retention_policy()
155
+
156
+ def apply_retention_policy(self):
157
+ """Remove entries older than 24 hours"""
158
+ cutoff_time = time.time() - 86400
159
+ for e in self.envelopes:
160
+ if e.timestamp < cutoff_time:
161
+ self.envelopes.remove(e)
162
+ else:
163
+ break
uagents_core/logger.py ADDED
@@ -0,0 +1,37 @@
1
+ import logging
2
+ import os
3
+
4
+ import structlog
5
+
6
+ _log_level_map = {
7
+ "NOTSET": logging.NOTSET,
8
+ "DEBUG": logging.DEBUG,
9
+ "INFO": logging.INFO,
10
+ "WARNING": logging.WARNING,
11
+ "ERROR": logging.ERROR,
12
+ "CRITICAL": logging.CRITICAL,
13
+ }
14
+
15
+ _log_level = os.getenv("LOG_LEVEL", "INFO")
16
+
17
+
18
+ structlog.configure(
19
+ processors=[
20
+ structlog.contextvars.merge_contextvars,
21
+ structlog.processors.add_log_level,
22
+ structlog.processors.StackInfoRenderer(),
23
+ structlog.dev.set_exc_info,
24
+ structlog.processors.TimeStamper(fmt="%Y-%m-%d %H:%M:%S", utc=False),
25
+ structlog.dev.ConsoleRenderer(),
26
+ ],
27
+ wrapper_class=structlog.make_filtering_bound_logger(
28
+ _log_level_map.get(_log_level, logging.INFO)
29
+ ),
30
+ context_class=dict,
31
+ logger_factory=structlog.PrintLoggerFactory(),
32
+ cache_logger_on_first_use=False,
33
+ )
34
+
35
+
36
+ def get_logger(logger_name: str):
37
+ return structlog.get_logger(logger_name)
uagents_core/models.py ADDED
@@ -0,0 +1,38 @@
1
+ import hashlib
2
+ from typing import Any, Dict, Type, Union
3
+
4
+ from pydantic.v1 import BaseModel, Field # noqa
5
+
6
+
7
+ # reverting back to pydantic v1 BaseModel for backwards compatibility
8
+ class Model(BaseModel):
9
+ @classmethod
10
+ def model_json_schema(cls) -> str:
11
+ return cls.schema_json()
12
+
13
+ def model_dump_json(self) -> str:
14
+ return self.json()
15
+
16
+ def model_dump(self) -> Dict[str, Any]:
17
+ return self.dict()
18
+
19
+ @classmethod
20
+ def model_validate_json(cls, obj: Any) -> "Model":
21
+ return cls.parse_raw(obj)
22
+
23
+ @classmethod
24
+ def model_validate(cls, obj: Union[Dict[str, Any], "Model"]) -> "Model":
25
+ return cls.parse_obj(obj)
26
+
27
+ @staticmethod
28
+ def build_schema_digest(model: Union["Model", Type["Model"]]) -> str:
29
+ schema = model.schema_json(indent=None, sort_keys=True)
30
+ digest = hashlib.sha256(schema.encode("utf8")).digest().hex()
31
+
32
+ return f"model:{digest}"
33
+
34
+
35
+ class ErrorMessage(Model):
36
+ """Error message model"""
37
+
38
+ error: str
@@ -0,0 +1,78 @@
1
+ import hashlib
2
+ import json
3
+ import time
4
+ from typing import Dict, List, Optional, Union
5
+
6
+ from pydantic import BaseModel, Field
7
+
8
+ from uagents_core.crypto import Identity
9
+ from uagents_core.types import AddressPrefix, AgentEndpoint, AgentType
10
+ from uagents_core.utils.communication import parse_identifier
11
+
12
+
13
+ class VerifiableModel(BaseModel):
14
+ agent_identifier: str
15
+ signature: Optional[str] = None
16
+ timestamp: Optional[int] = None
17
+
18
+ def sign(self, identity: Identity):
19
+ self.timestamp = int(time.time())
20
+ digest = self._build_digest()
21
+ self.signature = identity.sign_digest(digest)
22
+
23
+ def verify(self) -> bool:
24
+ _, _, agent_address = parse_identifier(self.agent_identifier)
25
+ return self.signature is not None and Identity.verify_digest(
26
+ agent_address, self._build_digest(), self.signature
27
+ )
28
+
29
+ def _build_digest(self) -> bytes:
30
+ sha256 = hashlib.sha256()
31
+ sha256.update(
32
+ json.dumps(
33
+ self.model_dump(exclude={"signature"}),
34
+ sort_keys=True,
35
+ separators=(",", ":"),
36
+ ).encode("utf-8")
37
+ )
38
+ return sha256.digest()
39
+
40
+
41
+ class AgentRegistrationAttestation(VerifiableModel):
42
+ protocols: List[str]
43
+ endpoints: List[AgentEndpoint]
44
+ metadata: Optional[Dict[str, Union[str, Dict[str, str]]]] = None
45
+
46
+
47
+ class RegistrationRequest(BaseModel):
48
+ address: str
49
+ prefix: Optional[AddressPrefix] = "test-agent"
50
+ challenge: str
51
+ challenge_response: str
52
+ agent_type: AgentType
53
+ endpoint: Optional[str] = None
54
+
55
+
56
+ class AgentverseConnectRequest(BaseModel):
57
+ user_token: str
58
+ agent_type: AgentType
59
+ endpoint: Optional[str] = None
60
+
61
+
62
+ class RegistrationResponse(BaseModel):
63
+ success: bool
64
+
65
+
66
+ class ChallengeRequest(BaseModel):
67
+ address: str
68
+
69
+
70
+ class ChallengeResponse(BaseModel):
71
+ challenge: str
72
+
73
+
74
+ class AgentUpdates(BaseModel):
75
+ name: str = Field(min_length=1, max_length=80)
76
+ readme: Optional[str] = Field(default=None, max_length=80000)
77
+ avatar_url: Optional[str] = Field(default=None, max_length=4000)
78
+ agent_type: Optional[AgentType] = "custom"
uagents_core/types.py ADDED
@@ -0,0 +1,14 @@
1
+ from typing import Literal
2
+
3
+ from pydantic import BaseModel
4
+
5
+ JsonStr = str
6
+
7
+ AgentType = Literal["mailbox", "proxy", "custom"]
8
+
9
+ AddressPrefix = Literal["agent", "test-agent"]
10
+
11
+
12
+ class AgentEndpoint(BaseModel):
13
+ url: str
14
+ weight: int
File without changes
@@ -0,0 +1,130 @@
1
+ import json
2
+ import urllib.parse
3
+ from typing import Any, List, Optional
4
+ from uuid import UUID, uuid4
5
+
6
+ import requests
7
+
8
+ from uagents_core.communication import parse_identifier, weighted_random_sample
9
+ from uagents_core.config import (
10
+ DEFAULT_ALMANAC_API_PATH,
11
+ DEFAULT_MAX_ENDPOINTS,
12
+ AgentverseConfig,
13
+ )
14
+ from uagents_core.crypto import Identity
15
+ from uagents_core.envelope import Envelope
16
+ from uagents_core.logger import get_logger
17
+
18
+ logger = get_logger("uagents_core.utils.communication")
19
+
20
+
21
+ def lookup_endpoint_for_agent(
22
+ agent_identifier: str,
23
+ *,
24
+ max_endpoints: int = DEFAULT_MAX_ENDPOINTS,
25
+ agentverse_config: Optional[AgentverseConfig] = None,
26
+ ) -> List[str]:
27
+ """
28
+ Look up 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("looking up endpoint for agent", extra=request_meta)
46
+ r = requests.get(f"{almanac_api}/agents/{agent_address}")
47
+ r.raise_for_status()
48
+
49
+ request_meta["response_status"] = r.status_code
50
+ logger.info(
51
+ "Got response looking up agent endpoint",
52
+ extra=request_meta,
53
+ )
54
+
55
+ endpoints = r.json().get("endpoints", [])
56
+
57
+ if len(endpoints) > 0:
58
+ endpoints = [val.get("url") for val in endpoints]
59
+ weights = [val.get("weight") for val in endpoints]
60
+ return weighted_random_sample(
61
+ endpoints,
62
+ weights=weights,
63
+ k=min(max_endpoints, len(endpoints)),
64
+ )
65
+
66
+ return []
67
+
68
+
69
+ def send_message(
70
+ destination: str,
71
+ message_schema_digest: str,
72
+ message_body: Any,
73
+ sender: Identity,
74
+ *,
75
+ session_id: Optional[UUID] = None,
76
+ protocol_digest: Optional[str] = None,
77
+ agentverse_config: Optional[AgentverseConfig] = None,
78
+ ):
79
+ """
80
+ Send a message (dict) to an agent.
81
+
82
+ Args:
83
+ destination (str): The address of the target agent.
84
+ message_schema_digest (str): The digest of the model that is being used
85
+ message_body (Any): The payload of the message.
86
+ sender (Identity): The identity of the sender.
87
+ session (UUID): The unique identifier for the dialogue between two agents
88
+ protocol_digest (str): The digest of the protocol that is being used
89
+ agentverse_config (AgentverseConfig): The configuration for the agentverse API
90
+ Returns:
91
+ None
92
+ """
93
+ json_payload = json.dumps(message_body, separators=(",", ":"))
94
+
95
+ env = Envelope(
96
+ version=1,
97
+ sender=sender.address,
98
+ target=destination,
99
+ session=session_id or uuid4(),
100
+ schema_digest=message_schema_digest,
101
+ protocol_digest=protocol_digest,
102
+ )
103
+
104
+ env.encode_payload(json_payload)
105
+ env.sign(sender)
106
+
107
+ logger.debug("Sending message to agent", extra={"envelope": env.model_dump()})
108
+
109
+ # query the almanac to lookup the destination agent
110
+ agentverse_config = agentverse_config or AgentverseConfig()
111
+ endpoints = lookup_endpoint_for_agent(
112
+ destination, agentverse_config=agentverse_config
113
+ )
114
+
115
+ if len(endpoints) == 0:
116
+ logger.error(
117
+ "No endpoints found for agent", extra={"agent_address": destination}
118
+ )
119
+ return
120
+
121
+ # send the envelope to the destination agent
122
+ request_meta = {"agent_address": destination, "agent_endpoint": endpoints[0]}
123
+ logger.debug("Sending message to agent", extra=request_meta)
124
+ r = requests.post(
125
+ endpoints[0],
126
+ headers={"content-type": "application/json"},
127
+ data=env.model_dump_json(),
128
+ )
129
+ r.raise_for_status()
130
+ logger.info("Sent message to agent", extra=request_meta)
@@ -0,0 +1,224 @@
1
+ import urllib.parse
2
+ from typing import List, Optional
3
+
4
+ import requests
5
+
6
+ from uagents_core.config import (
7
+ DEFAULT_ALMANAC_API_PATH,
8
+ DEFAULT_CHALLENGE_PATH,
9
+ DEFAULT_REGISTRATION_PATH,
10
+ AgentverseConfig,
11
+ )
12
+ from uagents_core.crypto import Identity
13
+ from uagents_core.logger import get_logger
14
+ from uagents_core.registration import (
15
+ AgentRegistrationAttestation,
16
+ AgentUpdates,
17
+ AgentverseConnectRequest,
18
+ ChallengeRequest,
19
+ ChallengeResponse,
20
+ RegistrationRequest,
21
+ RegistrationResponse,
22
+ )
23
+ from uagents_core.types import AgentEndpoint
24
+
25
+ logger = get_logger("uagents_core.utils.registration")
26
+
27
+
28
+ def register_in_almanac(
29
+ request: AgentverseConnectRequest,
30
+ identity: Identity,
31
+ *,
32
+ protocol_digests: List[str],
33
+ agentverse_config: Optional[AgentverseConfig] = None,
34
+ ):
35
+ """
36
+ Register the agent with the Almanac API.
37
+
38
+ Args:
39
+ request (AgentverseConnectRequest): The request containing the agent details.
40
+ identity (Identity): The identity of the agent.
41
+ protocol_digest (List[str]): The digest of the protocol that the agent supports
42
+ agentverse_config (AgentverseConfig): The configuration for the agentverse API
43
+ """
44
+
45
+ # get the almanac API endpoint
46
+ agentverse_config = agentverse_config or AgentverseConfig()
47
+ almanac_api = urllib.parse.urljoin(agentverse_config.url, DEFAULT_ALMANAC_API_PATH)
48
+
49
+ # get the agent address
50
+ agent_address = identity.address
51
+
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
+ # create the attestation
77
+ attestation = AgentRegistrationAttestation(
78
+ agent_identifier=agent_address,
79
+ protocols=protocol_digests,
80
+ endpoints=[
81
+ AgentEndpoint(url=request.endpoint, weight=1),
82
+ ],
83
+ metadata=None,
84
+ )
85
+
86
+ # sign the attestation
87
+ attestation.sign(identity)
88
+
89
+ # 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,
99
+ )
100
+
101
+
102
+ def register_in_agentverse(
103
+ request: AgentverseConnectRequest,
104
+ identity: Identity,
105
+ agent_details: Optional[AgentUpdates] = None,
106
+ *,
107
+ agentverse_config: Optional[AgentverseConfig] = None,
108
+ ):
109
+ """
110
+ Register the agent with the Agentverse API.
111
+
112
+ Args:
113
+ request (AgentverseConnectRequest): The request containing the agent details.
114
+ 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
119
+ """
120
+
121
+ # API endpoints
122
+ agentverse_config = agentverse_config or AgentverseConfig()
123
+ registration_api = urllib.parse.urljoin(
124
+ agentverse_config.url, DEFAULT_REGISTRATION_PATH
125
+ )
126
+ challenge_api = urllib.parse.urljoin(agentverse_config.url, DEFAULT_CHALLENGE_PATH)
127
+
128
+ # get the agent address
129
+ agent_address = identity.address
130
+
131
+ registration_metadata = {
132
+ "registration_api": registration_api,
133
+ "challenge_api": challenge_api,
134
+ "agent_address": agent_address,
135
+ "agent_endpoint": request.endpoint or "",
136
+ "agent_type": request.agent_type,
137
+ "agent_name": agent_details.name if agent_details else "",
138
+ }
139
+
140
+ # check to see if the agent exists
141
+ r = requests.get(
142
+ f"{registration_api}/{agent_address}",
143
+ headers={
144
+ "content-type": "application/json",
145
+ "authorization": f"Bearer {request.user_token}",
146
+ },
147
+ )
148
+
149
+ # if it doesn't then create it
150
+ if r.status_code == 404:
151
+ logger.debug(
152
+ "Agent did not exist on agentverse; registering it",
153
+ extra=registration_metadata,
154
+ )
155
+
156
+ challenge_request = ChallengeRequest(address=identity.address)
157
+ logger.debug(
158
+ "Requesting mailbox access challenge",
159
+ extra=registration_metadata,
160
+ )
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
+ },
168
+ )
169
+ r.raise_for_status()
170
+ challenge = ChallengeResponse.model_validate_json(r.text)
171
+ registration_payload = RegistrationRequest(
172
+ address=identity.address,
173
+ challenge=challenge.challenge,
174
+ challenge_response=identity.sign(challenge.challenge.encode()),
175
+ endpoint=request.endpoint,
176
+ 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
+ },
184
+ data=registration_payload,
185
+ )
186
+ if r.status_code == 409:
187
+ logger.info(
188
+ "Agent already registered with Agentverse",
189
+ extra=registration_metadata,
190
+ )
191
+ else:
192
+ r.raise_for_status()
193
+ registration_response = RegistrationResponse.model_validate_json(r.text)
194
+ if registration_response.success:
195
+ logger.info(
196
+ f"Successfully registered as {request.agent_type} agent in Agentverse",
197
+ extra=registration_metadata,
198
+ )
199
+ if not agent_details:
200
+ logger.debug(
201
+ "No agent details provided; skipping agent update",
202
+ extra=registration_metadata,
203
+ )
204
+ return
205
+
206
+ # update the readme and the title of the agent to make it easier to find
207
+ 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",
223
+ extra=registration_metadata,
224
+ )
@@ -0,0 +1,27 @@
1
+ Metadata-Version: 2.1
2
+ Name: uagents-core
3
+ Version: 0.1.0
4
+ Summary: Core components for agent based systems
5
+ License: Apache 2.0
6
+ Author: Ed FitzGerald
7
+ Author-email: edward.fitzgerald@fetch.ai
8
+ Requires-Python: >=3.9,<3.13
9
+ Classifier: License :: Other/Proprietary License
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Programming Language :: Python :: 3.9
12
+ Classifier: Programming Language :: Python :: 3.10
13
+ Classifier: Programming Language :: Python :: 3.11
14
+ 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)
19
+ Requires-Dist: pydantic (>=2.8,<2.9)
20
+ 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)
23
+ Description-Content-Type: text/markdown
24
+
25
+ # UAgents-Core
26
+
27
+ Core definitions and functionalities to build agent which can interact and integrate with Fetch.ai ecosystem and agent marketplace.
@@ -0,0 +1,15 @@
1
+ uagents_core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ uagents_core/communication.py,sha256=-QinbuCxEHuL79cRkMcfJ3FBPa0XNsU_KeudOhtBIsY,2067
3
+ uagents_core/config.py,sha256=YLZo19ciWi83J1F4EEKOsc1SoMrcPs_5GmOHt-rxQvk,514
4
+ uagents_core/crypto.py,sha256=oLbKrVPiZ52zz4vrvRQIr7gNyFmr34t-8KvZTlGqXU4,4833
5
+ uagents_core/envelope.py,sha256=Lh8Uw85p8sdHncLV-F_ExAyyHxlc9915jQhP7bpgBZM,4931
6
+ uagents_core/logger.py,sha256=FH0pUINtXubRNsGxsZ0b2Vu-19zxa7DRshB8_fWbVGQ,945
7
+ uagents_core/models.py,sha256=DhDQQQJ34QMHhLCO7nAJl5td7IdaIK8pAaoLhXU3VhA,1002
8
+ uagents_core/registration.py,sha256=42xyyEzgW3hPx197KyOTYDrhFNu9uGOcRpo34EXmcA0,2134
9
+ uagents_core/types.py,sha256=2tV23954ADupIp0SggLSO4PG8kTRXw9uHzfqMgbcdJk,236
10
+ uagents_core/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
11
+ uagents_core/utils/communication.py,sha256=9mw4lc5y_fUtRUfAl0Dkk10HcEJAefWaXl-BXbs7n74,4010
12
+ uagents_core/utils/registration.py,sha256=1NPvpm20f3LE9LYJb2HP-SyEPlPa7ADM2HXmvHhx0fQ,7165
13
+ uagents_core-0.1.0.dist-info/METADATA,sha256=RuVO-mne7GsExdPVhx4tflWAOFJHVqwJcXT71iLXfU0,1025
14
+ uagents_core-0.1.0.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
15
+ uagents_core-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: poetry-core 1.9.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any