uagents-core 0.1.2__tar.gz → 0.2.0__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.1.2 → uagents_core-0.2.0}/PKG-INFO +11 -11
- {uagents_core-0.1.2 → uagents_core-0.2.0}/README.md +2 -2
- uagents_core-0.2.0/pyproject.toml +62 -0
- {uagents_core-0.1.2 → uagents_core-0.2.0}/uagents_core/config.py +12 -1
- uagents_core-0.2.0/uagents_core/contrib/protocols/chat/__init__.py +124 -0
- uagents_core-0.2.0/uagents_core/contrib/protocols/subscriptions/__init__.py +75 -0
- {uagents_core-0.1.2 → uagents_core-0.2.0}/uagents_core/envelope.py +19 -71
- uagents_core-0.2.0/uagents_core/helpers.py +30 -0
- uagents_core-0.1.2/uagents_core/crypto.py → uagents_core-0.2.0/uagents_core/identity.py +57 -7
- uagents_core-0.2.0/uagents_core/logger.py +36 -0
- {uagents_core-0.1.2 → uagents_core-0.2.0}/uagents_core/models.py +9 -5
- uagents_core-0.2.0/uagents_core/protocol.py +166 -0
- uagents_core-0.2.0/uagents_core/registration.py +109 -0
- uagents_core-0.2.0/uagents_core/types.py +50 -0
- uagents_core-0.2.0/uagents_core/utils/__init__.py +5 -0
- uagents_core-0.2.0/uagents_core/utils/messages.py +153 -0
- uagents_core-0.2.0/uagents_core/utils/registration.py +272 -0
- uagents_core-0.2.0/uagents_core/utils/resolver.py +73 -0
- uagents_core-0.1.2/pyproject.toml +0 -67
- uagents_core-0.1.2/uagents_core/communication.py +0 -76
- uagents_core-0.1.2/uagents_core/logger.py +0 -37
- uagents_core-0.1.2/uagents_core/registration.py +0 -78
- uagents_core-0.1.2/uagents_core/types.py +0 -14
- uagents_core-0.1.2/uagents_core/utils/__init__.py +0 -0
- uagents_core-0.1.2/uagents_core/utils/communication.py +0 -130
- uagents_core-0.1.2/uagents_core/utils/registration.py +0 -224
- {uagents_core-0.1.2 → uagents_core-0.2.0}/uagents_core/__init__.py +0 -0
@@ -1,27 +1,27 @@
|
|
1
1
|
Metadata-Version: 2.3
|
2
2
|
Name: uagents-core
|
3
|
-
Version: 0.
|
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.
|
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
|
-
|
16
|
-
Requires-Dist: bech32 (>=1.2.0,<2.0
|
17
|
-
Requires-Dist: ecdsa (>=0.19.0,<
|
18
|
-
Requires-Dist:
|
19
|
-
Requires-Dist: pydantic (>=2.8,<2.9)
|
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)
|
17
|
+
Requires-Dist: pydantic (>=2.8,<3.0)
|
20
18
|
Requires-Dist: requests (>=2.32.3,<3.0)
|
21
|
-
|
22
|
-
|
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
|
-
#
|
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
|
+
|
@@ -1,3 +1,3 @@
|
|
1
|
-
#
|
1
|
+
# uAgents-Core
|
2
2
|
|
3
|
-
Core definitions and functionalities to build agent which can interact and integrate with Fetch.ai ecosystem and agent marketplace.
|
3
|
+
Core definitions and functionalities to build agent which can interact and integrate with Fetch.ai ecosystem and agent marketplace.
|
@@ -0,0 +1,62 @@
|
|
1
|
+
[project]
|
2
|
+
name = "uagents-core"
|
3
|
+
version = "0.2.0"
|
4
|
+
description = "Core components for agent based systems"
|
5
|
+
authors = [
|
6
|
+
{ name = "Ed FitzGerald", email = "edward.fitzgerald@fetch.ai" },
|
7
|
+
{ name = "James Riehl", email = "james.riehl@fetch.ai" },
|
8
|
+
{ name = "Alejandro Morales", email = "alejandro.madrigal@fetch.ai" },
|
9
|
+
{ name = "Florian Wilde", email = "florian.wilde@fetch.ai" },
|
10
|
+
{ name = "Attila Bagoly", email = "attila.bagoly@fetch.ai" },
|
11
|
+
]
|
12
|
+
license = { text = "Apache 2.0" }
|
13
|
+
readme = "README.md"
|
14
|
+
requires-python = ">=3.10,<4.0"
|
15
|
+
dependencies = [
|
16
|
+
"pydantic (>=2.8,<3.0)",
|
17
|
+
"bech32 (>=1.2.0,<2.0)",
|
18
|
+
"ecdsa (>=0.19.0,<1.0)",
|
19
|
+
"requests (>=2.32.3,<3.0)",
|
20
|
+
]
|
21
|
+
|
22
|
+
[project.urls]
|
23
|
+
homepage = "https://fetch.ai"
|
24
|
+
repository = "https://github.com/fetchai/uAgents"
|
25
|
+
documentation = "https://fetch.ai/docs"
|
26
|
+
|
27
|
+
[tool.poetry]
|
28
|
+
packages = [{ include = "uagents_core" }]
|
29
|
+
|
30
|
+
[tool.poetry.group.dev.dependencies]
|
31
|
+
ruff = "^0.11.0"
|
32
|
+
pre-commit = "^4.1.0"
|
33
|
+
|
34
|
+
[build-system]
|
35
|
+
requires = ["poetry-core>=1.0.0"]
|
36
|
+
build-backend = "poetry.core.masonry.api"
|
37
|
+
|
38
|
+
[tool.ruff]
|
39
|
+
target-version = "py312"
|
40
|
+
|
41
|
+
[tool.ruff.lint]
|
42
|
+
select = [
|
43
|
+
# pycodestyle (Errors, Warnings)
|
44
|
+
"E",
|
45
|
+
"W",
|
46
|
+
# Pyflakes
|
47
|
+
"F",
|
48
|
+
# flake8-bugbear
|
49
|
+
"B",
|
50
|
+
# flake8-simplify
|
51
|
+
"SIM",
|
52
|
+
# isort
|
53
|
+
"I",
|
54
|
+
# pep8-naming
|
55
|
+
"N",
|
56
|
+
# pylint
|
57
|
+
"PL",
|
58
|
+
]
|
59
|
+
ignore = ["PLR0913", "PLR0912", "PLR0911", "PLR2004", "PLR0915"]
|
60
|
+
|
61
|
+
[tool.ruff.lint.pycodestyle]
|
62
|
+
max-line-length = 100
|
@@ -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
|
+
)
|
@@ -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.
|
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 (
|
24
|
+
protocol_digest (str | None): The digest of the protocol associated with the message
|
32
25
|
(optional).
|
33
|
-
payload (
|
34
|
-
expires (
|
35
|
-
nonce (
|
36
|
-
signature (
|
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:
|
45
|
-
payload:
|
46
|
-
expires:
|
47
|
-
nonce:
|
48
|
-
signature:
|
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
|
66
|
+
Sign the envelope using the provided agent identity.
|
74
67
|
|
75
68
|
Args:
|
76
|
-
identity (Identity): The identity to
|
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(
|
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) ->
|
16
|
+
def _decode_bech32(value: str) -> tuple[str, bytes]:
|
16
17
|
prefix, data_base5 = bech32.bech32_decode(value)
|
17
|
-
|
18
|
-
|
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:
|
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(
|
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
|
@@ -0,0 +1,36 @@
|
|
1
|
+
import logging
|
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",
|
10
|
+
)
|
11
|
+
|
12
|
+
|
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
|
@@ -1,7 +1,8 @@
|
|
1
1
|
import hashlib
|
2
|
-
from typing import Any
|
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) ->
|
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) ->
|
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:
|
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:
|
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)
|