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.
uagents_core/config.py CHANGED
@@ -4,18 +4,29 @@ DEFAULT_AGENTVERSE_URL = "agentverse.ai"
4
4
  DEFAULT_ALMANAC_API_PATH = "/v1/almanac"
5
5
  DEFAULT_REGISTRATION_PATH = "/v1/agents"
6
6
  DEFAULT_CHALLENGE_PATH = "/v1/auth/challenge"
7
+ DEFAULT_MAILBOX_PATH = "/v1/submit"
8
+ DEFAULT_PROXY_PATH = "/v1/proxy/submit"
7
9
 
8
10
  DEFAULT_MAX_ENDPOINTS = 10
9
11
 
12
+ DEFAULT_REQUEST_TIMEOUT = 10
13
+
10
14
  AGENT_ADDRESS_LENGTH = 65
11
15
  AGENT_PREFIX = "agent"
12
16
 
13
17
 
14
18
  class AgentverseConfig(BaseModel):
15
19
  base_url: str = DEFAULT_AGENTVERSE_URL
16
- protocol: str = "https"
17
20
  http_prefix: str = "https"
18
21
 
19
22
  @property
20
23
  def url(self) -> str:
21
24
  return f"{self.http_prefix}://{self.base_url}"
25
+
26
+ @property
27
+ def mailbox_endpoint(self) -> str:
28
+ return f"{self.url}{DEFAULT_MAILBOX_PATH}"
29
+
30
+ @property
31
+ def proxy_endpoint(self) -> str:
32
+ return f"{self.url}{DEFAULT_PROXY_PATH}"
@@ -0,0 +1,124 @@
1
+ """
2
+ This module contains the protocol specification for the agent chat protocol.
3
+ """
4
+
5
+ from datetime import datetime
6
+ from typing import Literal, TypedDict
7
+
8
+ from pydantic.v1 import UUID4
9
+
10
+ from uagents_core.models import Model
11
+ from uagents_core.protocol import ProtocolSpecification
12
+
13
+
14
+ class Metadata(TypedDict):
15
+ # primarily used with the `Resource` model. This field specifies the mime_type of
16
+ # resource that is being referenced. A full list can be found at `docs/mime_types.md`
17
+ mime_type: str
18
+
19
+ # the role of the resource
20
+ role: str
21
+
22
+
23
+ class TextContent(Model):
24
+ type: Literal["text"]
25
+
26
+ # The text of the content. The format of this field is UTF-8 encoded strings. Additionally,
27
+ # markdown based formatting can be used and will be supported by most clients
28
+ text: str
29
+
30
+
31
+ class Resource(Model):
32
+ # the uri of the resource
33
+ uri: str
34
+
35
+ # the set of metadata for this resource, for more detailed description of the set of
36
+ # fields see `docs/metadata.md`
37
+ metadata: dict[str, str]
38
+
39
+
40
+ class ResourceContent(Model):
41
+ type: Literal["resource"]
42
+
43
+ # The resource id
44
+ resource_id: UUID4
45
+
46
+ # The resource or list of resource for this content. typically only a single
47
+ # resource will be sent, however, if there are accompanying resources like
48
+ # thumbnails and audio tracks these can be additionally referenced
49
+ #
50
+ # In the case of the a list of resources, the first element of the list is always
51
+ # considered the primary resource
52
+ resource: Resource | list[Resource]
53
+
54
+
55
+ class MetadataContent(Model):
56
+ type: Literal["metadata"]
57
+
58
+ # the set of metadata for this content, for more detailed description of the set of
59
+ # fields see `docs/metadata.md`
60
+ metadata: dict[str, str]
61
+
62
+
63
+ class StartSessionContent(Model):
64
+ type: Literal["start-session"]
65
+
66
+
67
+ class EndSessionContent(Model):
68
+ type: Literal["end-session"]
69
+
70
+
71
+ class StartStreamContent(Model):
72
+ type: Literal["start-stream"]
73
+
74
+ stream_id: UUID4
75
+
76
+
77
+ class EndStreamContent(Model):
78
+ type: Literal["end-stream"]
79
+
80
+ stream_id: UUID4
81
+
82
+
83
+ # The combined agent content types
84
+ AgentContent = (
85
+ TextContent
86
+ | ResourceContent
87
+ | MetadataContent
88
+ | StartSessionContent
89
+ | EndSessionContent
90
+ | StartStreamContent
91
+ | EndStreamContent
92
+ )
93
+
94
+
95
+ class ChatMessage(Model):
96
+ # the timestamp for the message, should be in UTC
97
+ timestamp: datetime
98
+
99
+ # a unique message id that is generated from the message instigator
100
+ msg_id: UUID4
101
+
102
+ # the list of content elements in the chat
103
+ content: list[AgentContent]
104
+
105
+
106
+ class ChatAcknowledgement(Model):
107
+ # the timestamp for the message, should be in UTC
108
+ timestamp: datetime
109
+
110
+ # the msg id that is being acknowledged
111
+ acknowledged_msg_id: UUID4
112
+
113
+ # optional acknowledgement metadata
114
+ metadata: dict[str, str] | None = None
115
+
116
+
117
+ chat_protocol_spec = ProtocolSpecification(
118
+ name="AgentChatProtocol",
119
+ version="0.3.0",
120
+ interactions={
121
+ ChatMessage: {ChatAcknowledgement},
122
+ ChatAcknowledgement: set(),
123
+ },
124
+ )
@@ -0,0 +1,75 @@
1
+ """
2
+ This module contains the protocol specification for agent subscription management.
3
+
4
+ A chat agent that supports this protocol will be able to accept or reject paid
5
+ subscription requests from other agents.
6
+ """
7
+
8
+ from enum import Enum
9
+
10
+ from uagents_core.models import Model
11
+ from uagents_core.protocol import ProtocolSpecification
12
+
13
+
14
+ class TierType(str, Enum):
15
+ FREE = "free"
16
+ PLUS = "plus"
17
+ PRO = "pro"
18
+
19
+
20
+ class Tier(Model):
21
+ tier_id: str
22
+ tier_type: TierType
23
+ amount: str
24
+ currency: str
25
+
26
+
27
+ class RequestAgentSubscriptionRenewal(Model):
28
+ # the system generated event id associated with this upgrade
29
+ update_id: str
30
+
31
+ # the tier this upgrade is subscribing to
32
+ tier: Tier
33
+
34
+
35
+ class RequestAgentSubscriptionUpgrade(Model):
36
+ # the system generated event id associated with this upgrade
37
+ update_id: str
38
+
39
+ # the tier this upgrade is subscribing to
40
+ new_subscription: Tier
41
+
42
+ # previous subscription, None if it was the 'free' type
43
+ existing_subscription: Tier | None = None
44
+
45
+
46
+ class AcceptAgentSubscriptionUpdate(Model):
47
+ # the update id associated with this upgrade
48
+ update_id: str
49
+
50
+
51
+ class RejectAgentSubscriptionUpdate(Model):
52
+ # the update id associated with this upgrade
53
+ update_id: str
54
+
55
+
56
+ subscription_protocol_spec = ProtocolSpecification(
57
+ name="AgentSubscriptionProtocol",
58
+ version="0.2.0",
59
+ interactions={
60
+ RequestAgentSubscriptionRenewal: {
61
+ AcceptAgentSubscriptionUpdate,
62
+ RejectAgentSubscriptionUpdate,
63
+ },
64
+ RequestAgentSubscriptionUpgrade: {
65
+ AcceptAgentSubscriptionUpdate,
66
+ RejectAgentSubscriptionUpdate,
67
+ },
68
+ AcceptAgentSubscriptionUpdate: set(),
69
+ RejectAgentSubscriptionUpdate: set(),
70
+ },
71
+ roles={
72
+ "requester": {AcceptAgentSubscriptionUpdate, RejectAgentSubscriptionUpdate},
73
+ "agent": {RequestAgentSubscriptionRenewal, RequestAgentSubscriptionUpgrade},
74
+ },
75
+ )
uagents_core/envelope.py CHANGED
@@ -3,17 +3,10 @@
3
3
  import base64
4
4
  import hashlib
5
5
  import struct
6
- import time
7
- from typing import List, Optional
8
6
 
9
- from pydantic import (
10
- UUID4,
11
- BaseModel,
12
- Field,
13
- field_serializer,
14
- )
7
+ from pydantic import UUID4, BaseModel
15
8
 
16
- from uagents_core.crypto import Identity
9
+ from uagents_core.identity import Identity
17
10
  from uagents_core.types import JsonStr
18
11
 
19
12
 
@@ -28,12 +21,12 @@ class Envelope(BaseModel):
28
21
  session (UUID4): The session UUID that persists for back-and-forth
29
22
  dialogues between agents.
30
23
  schema_digest (str): The schema digest for the enclosed message.
31
- protocol_digest (Optional[str]): The digest of the protocol associated with the message
24
+ protocol_digest (str | None): The digest of the protocol associated with the message
32
25
  (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).
26
+ payload (str | None): The encoded message payload of the envelope (optional).
27
+ expires (int | None): The expiration timestamp (optional).
28
+ nonce (int | None): The nonce value (optional).
29
+ signature (str | None): The envelope signature (optional).
37
30
  """
38
31
 
39
32
  version: int
@@ -41,13 +34,13 @@ class Envelope(BaseModel):
41
34
  target: str
42
35
  session: UUID4
43
36
  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
37
+ protocol_digest: str | None = None
38
+ payload: str | None = None
39
+ expires: int | None = None
40
+ nonce: int | None = None
41
+ signature: str | None = None
49
42
 
50
- def encode_payload(self, value: JsonStr):
43
+ def encode_payload(self, value: JsonStr) -> None:
51
44
  """
52
45
  Encode the payload value and store it in the envelope.
53
46
 
@@ -68,15 +61,12 @@ class Envelope(BaseModel):
68
61
 
69
62
  return base64.b64decode(self.payload).decode()
70
63
 
71
- def sign(self, identity: Identity):
64
+ def sign(self, identity: Identity) -> None:
72
65
  """
73
- Sign the envelope with the provided identity.
66
+ Sign the envelope using the provided agent identity.
74
67
 
75
68
  Args:
76
- identity (Identity): The identity to use for signing.
77
-
78
- Raises:
79
- ValueError: If the signature cannot be computed.
69
+ identity (Identity): The agent identity to sign the envelope.
80
70
  """
81
71
  try:
82
72
  self.signature = identity.sign_digest(self._digest())
@@ -96,7 +86,9 @@ class Envelope(BaseModel):
96
86
  """
97
87
  if self.signature is None:
98
88
  raise ValueError("Envelope signature is missing")
99
- return Identity.verify_digest(self.sender, self._digest(), self.signature)
89
+ return Identity.verify_digest(
90
+ address=self.sender, digest=self._digest(), signature=self.signature
91
+ )
100
92
 
101
93
  def _digest(self) -> bytes:
102
94
  """
@@ -117,47 +109,3 @@ class Envelope(BaseModel):
117
109
  if self.nonce is not None:
118
110
  hasher.update(struct.pack(">Q", self.nonce))
119
111
  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
@@ -0,0 +1,30 @@
1
+ from random import Random
2
+ from typing import Any
3
+
4
+
5
+ def weighted_random_sample(
6
+ items: list[Any],
7
+ weights: list[float] | None = None,
8
+ k: int = 1,
9
+ rng: Random | None = None,
10
+ ) -> list[Any]:
11
+ """
12
+ Weighted random sample from a list of items without replacement.
13
+
14
+ Ref: Efraimidis, Pavlos S. "Weighted random sampling over data streams."
15
+
16
+ Args:
17
+ items (list[Any]): The list of items to sample from.
18
+ weights (list[float]] | None) The optional list of weights for each item.
19
+ k (int): The number of items to sample.
20
+ rng (Random): The random number generator.
21
+
22
+ Returns:
23
+ list[Any]: The sampled items.
24
+ """
25
+ rng = rng or Random()
26
+ if weights is None:
27
+ return rng.sample(items, k=k)
28
+ values: list[Any] = [rng.random() ** (1 / w) for w in weights]
29
+ order: list[int] = sorted(range(len(items)), key=lambda i: values[i])
30
+ return [items[i] for i in order[-k:]]
@@ -2,24 +2,31 @@ import base64
2
2
  import hashlib
3
3
  import struct
4
4
  from secrets import token_bytes
5
- from typing import Tuple, Union
6
5
 
7
6
  import bech32
8
7
  import ecdsa
9
8
  from ecdsa.util import sigencode_string_canonize
10
9
 
10
+ from uagents_core.config import AGENT_ADDRESS_LENGTH, AGENT_PREFIX
11
+
11
12
  USER_PREFIX = "user"
12
13
  SHA_LENGTH = 256
13
14
 
14
15
 
15
- def _decode_bech32(value: str) -> Tuple[str, bytes]:
16
+ def _decode_bech32(value: str) -> tuple[str, bytes]:
16
17
  prefix, data_base5 = bech32.bech32_decode(value)
17
- data = bytes(bech32.convertbits(data_base5, 5, 8, False))
18
- return prefix, data
18
+ if not data_base5 or not prefix:
19
+ raise ValueError("Unable to decode value")
20
+ converted = bech32.convertbits(data_base5, 5, 8, False)
21
+ if not converted:
22
+ raise ValueError("Unable to convert value")
23
+ return prefix, bytes(converted)
19
24
 
20
25
 
21
26
  def _encode_bech32(prefix: str, value: bytes) -> str:
22
27
  value_base5 = bech32.convertbits(value, 8, 5)
28
+ if not value_base5:
29
+ raise ValueError("Unable to convert value")
23
30
  return bech32.bech32_encode(prefix, value_base5)
24
31
 
25
32
 
@@ -28,7 +35,7 @@ def is_user_address(address: str) -> bool:
28
35
 
29
36
 
30
37
  def generate_user_address() -> str:
31
- return _encode_bech32(USER_PREFIX, token_bytes(32))
38
+ return _encode_bech32(prefix=USER_PREFIX, value=token_bytes(32))
32
39
 
33
40
 
34
41
  def _key_derivation_hash(prefix: str, index: int) -> bytes:
@@ -52,7 +59,7 @@ def derive_key_from_seed(seed, prefix, index) -> bytes:
52
59
  return hasher.digest()
53
60
 
54
61
 
55
- def encode_length_prefixed(value: Union[str, int, bytes]) -> bytes:
62
+ def encode_length_prefixed(value: str | int | bytes) -> bytes:
56
63
  if isinstance(value, str):
57
64
  encoded = value.encode()
58
65
  elif isinstance(value, int):
@@ -76,7 +83,7 @@ class Identity:
76
83
  self._sk = signing_key
77
84
 
78
85
  # build the address
79
- pub_key_bytes = self._sk.get_verifying_key().to_string(encoding="compressed")
86
+ pub_key_bytes = self._sk.get_verifying_key().to_string("compressed") # type: ignore
80
87
  self._address = _encode_bech32("agent", pub_key_bytes)
81
88
  self._pub_key = pub_key_bytes.hex()
82
89
 
@@ -155,3 +162,46 @@ class Identity:
155
162
  verifying_key = ecdsa.VerifyingKey.from_string(pk_data, curve=ecdsa.SECP256k1)
156
163
 
157
164
  return verifying_key.verify_digest(sig_data, digest)
165
+
166
+
167
+ def is_valid_address(address: str) -> bool:
168
+ """
169
+ Check if the given string is a valid address.
170
+
171
+ Args:
172
+ address (str): The address to be checked.
173
+
174
+ Returns:
175
+ bool: True if the address is valid; False otherwise.
176
+ """
177
+ return is_user_address(address) or (
178
+ len(address) == AGENT_ADDRESS_LENGTH and address.startswith(AGENT_PREFIX)
179
+ )
180
+
181
+
182
+ def parse_identifier(identifier: str) -> tuple[str, str, str]:
183
+ """
184
+ Parse an agent identifier string into prefix, name, and address.
185
+
186
+ Args:
187
+ identifier (str): The identifier string to be parsed.
188
+
189
+ Returns:
190
+ tuple[str, str, str]: A Tuple containing the prefix, name, and address as strings.
191
+ """
192
+ prefix = ""
193
+ name = ""
194
+ address = ""
195
+
196
+ if "://" in identifier:
197
+ prefix, identifier = identifier.split("://", 1)
198
+
199
+ if "/" in identifier:
200
+ name, identifier = identifier.split("/", 1)
201
+
202
+ if is_valid_address(identifier):
203
+ address = identifier
204
+ else:
205
+ name = identifier
206
+
207
+ return prefix, name, address
uagents_core/logger.py CHANGED
@@ -1,37 +1,36 @@
1
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,
2
+ import sys
3
+
4
+ logging.basicConfig(level=logging.INFO)
5
+
6
+
7
+ formatter = logging.Formatter(
8
+ fmt="%(asctime)s,%(msecs)03d %(name)s %(levelname)s %(message)s",
9
+ datefmt="%Y-%m-%d %H:%M:%S",
33
10
  )
34
11
 
35
12
 
36
- def get_logger(logger_name: str):
37
- return structlog.get_logger(logger_name)
13
+ def get_logger(
14
+ logger_name: str | None = None,
15
+ level: int | str = logging.INFO,
16
+ log_file: str | None = "uagents_core.log",
17
+ ) -> logging.Logger:
18
+ """
19
+ Get a logger with the given name.
20
+
21
+ If no name is specified, the root logger is returned.
22
+ """
23
+ logger = logging.getLogger(logger_name)
24
+ logger.setLevel(level)
25
+
26
+ log_handler = logging.StreamHandler(sys.stdout)
27
+ log_handler.setFormatter(formatter)
28
+ logger.addHandler(log_handler)
29
+
30
+ if log_file:
31
+ log_handler = logging.FileHandler(log_file)
32
+ log_handler.setFormatter(formatter)
33
+ logger.addHandler(log_handler)
34
+
35
+ logger.propagate = False
36
+ return logger
uagents_core/models.py CHANGED
@@ -1,7 +1,8 @@
1
1
  import hashlib
2
- from typing import Any, Dict, Type, Union
2
+ from typing import Any
3
3
 
4
4
  from pydantic.v1 import BaseModel, Field # noqa
5
+ from typing_extensions import Self
5
6
 
6
7
 
7
8
  # reverting back to pydantic v1 BaseModel for backwards compatibility
@@ -13,19 +14,19 @@ class Model(BaseModel):
13
14
  def model_dump_json(self) -> str:
14
15
  return self.json()
15
16
 
16
- def model_dump(self) -> Dict[str, Any]:
17
+ def model_dump(self) -> dict[str, Any]:
17
18
  return self.dict()
18
19
 
19
20
  @classmethod
20
- def model_validate_json(cls, obj: Any) -> "Model":
21
+ def model_validate_json(cls, obj: Any) -> Self:
21
22
  return cls.parse_raw(obj)
22
23
 
23
24
  @classmethod
24
- def model_validate(cls, obj: Union[Dict[str, Any], "Model"]) -> "Model":
25
+ def model_validate(cls, obj: dict[str, Any] | Self) -> Self:
25
26
  return cls.parse_obj(obj)
26
27
 
27
28
  @staticmethod
28
- def build_schema_digest(model: Union["Model", Type["Model"]]) -> str:
29
+ def build_schema_digest(model: BaseModel | type[BaseModel]) -> str:
29
30
  schema = model.schema_json(indent=None, sort_keys=True)
30
31
  digest = hashlib.sha256(schema.encode("utf8")).digest().hex()
31
32
 
@@ -36,3 +37,6 @@ class ErrorMessage(Model):
36
37
  """Error message model"""
37
38
 
38
39
  error: str
40
+
41
+
42
+ ERROR_MESSAGE_DIGEST = Model.build_schema_digest(ErrorMessage)