Skip to content

Commit

Permalink
feat(core): add agent metadata property (#549)
Browse files Browse the repository at this point in the history
  • Loading branch information
Archento authored Oct 10, 2024
1 parent 04fc739 commit f31be9f
Show file tree
Hide file tree
Showing 7 changed files with 153 additions and 24 deletions.
21 changes: 20 additions & 1 deletion python/docs/api/uagents/agent.md
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,7 @@ An agent that interacts within a communication environment.
- `_ctx` _Context_ - The context for agent interactions.
- `_test` _bool_ - True if the agent will register and transact on the testnet.
- `_enable_agent_inspector` _bool_ - Enable the agent inspector REST endpoints.
- `_metadata` _Dict[str, Any]_ - Metadata associated with the agent.
Properties:
- `name` _str_ - The name of the agent.
Expand All @@ -172,6 +173,7 @@ An agent that interacts within a communication environment.
- `mailbox_client` _MailboxClient_ - The client for interacting with the agentverse mailbox.
- `protocols` _Dict[str, Protocol]_ - Dictionary mapping all supported protocol digests to their
corresponding protocols.
- `metadata` _Dict[str, Any]_ - Metadata associated with the agent.
<a id="src.uagents.agent.Agent.__init__"></a>
Expand All @@ -193,7 +195,8 @@ def __init__(name: Optional[str] = None,
test: bool = True,
loop: Optional[asyncio.AbstractEventLoop] = None,
log_level: Union[int, str] = logging.INFO,
enable_agent_inspector: bool = True)
enable_agent_inspector: bool = True,
metadata: Optional[Dict[str, Any]] = None)
```
Initialize an Agent instance.
Expand All @@ -217,6 +220,7 @@ Initialize an Agent instance.
- `loop` _Optional[asyncio.AbstractEventLoop]_ - The asyncio event loop to use.
- `log_level` _Union[int, str]_ - The logging level for the agent.
- `enable_agent_inspector` _bool_ - Enable the agent inspector for debugging.
- `metadata` _Optional[Dict[str, Any]]_ - Optional metadata to include in the agent object.
<a id="src.uagents.agent.Agent.initialize_wallet_messaging"></a>
Expand Down Expand Up @@ -385,6 +389,21 @@ Get the balance of the agent.
- `int` - Bank balance.
<a id="src.uagents.agent.Agent.metadata"></a>
#### metadata
```python
@property
def metadata() -> Dict[str, Any]
```
Get the metadata associated with the agent.
**Returns**:
Dict[str, Any]: The metadata associated with the agent.
<a id="src.uagents.agent.Agent.mailbox"></a>
#### mailbox
Expand Down
19 changes: 19 additions & 0 deletions python/docs/api/uagents/types.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,25 @@

# src.uagents.types

<a id="src.uagents.types.AgentGeolocation"></a>

## AgentGeolocation Objects

```python
class AgentGeolocation(BaseModel)
```

<a id="src.uagents.types.AgentGeolocation.serialize_precision"></a>

#### serialize`_`precision

```python
@field_serializer("latitude", "longitude")
def serialize_precision(val: float) -> float
```

Round the latitude and longitude to 6 decimal places.

<a id="src.uagents.types.DeliveryStatus"></a>

## DeliveryStatus Objects
Expand Down
45 changes: 44 additions & 1 deletion python/src/uagents/agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@
from uagents.types import (
AgentEndpoint,
AgentInfo,
AgentMetadata,
EventCallback,
IntervalCallback,
JsonStr,
Expand Down Expand Up @@ -248,6 +249,7 @@ class Agent(Sink):
_ctx (Context): The context for agent interactions.
_test (bool): True if the agent will register and transact on the testnet.
_enable_agent_inspector (bool): Enable the agent inspector REST endpoints.
_metadata (Dict[str, Any]): Metadata associated with the agent.
Properties:
name (str): The name of the agent.
Expand All @@ -260,6 +262,7 @@ class Agent(Sink):
mailbox_client (MailboxClient): The client for interacting with the agentverse mailbox.
protocols (Dict[str, Protocol]): Dictionary mapping all supported protocol digests to their
corresponding protocols.
metadata (Dict[str, Any]): Metadata associated with the agent.
"""

Expand All @@ -281,6 +284,7 @@ def __init__(
loop: Optional[asyncio.AbstractEventLoop] = None,
log_level: Union[int, str] = logging.INFO,
enable_agent_inspector: bool = True,
metadata: Optional[Dict[str, Any]] = None,
):
"""
Initialize an Agent instance.
Expand All @@ -303,6 +307,7 @@ def __init__(
loop (Optional[asyncio.AbstractEventLoop]): The asyncio event loop to use.
log_level (Union[int, str]): The logging level for the agent.
enable_agent_inspector (bool): Enable the agent inspector for debugging.
metadata (Optional[Dict[str, Any]]): Optional metadata to include in the agent object.
"""
self._init_done = False
self._name = name
Expand Down Expand Up @@ -375,6 +380,7 @@ def __init__(
logger=self._logger,
almanac_api=almanac_api_url,
)
self._metadata = self._initialize_metadata(metadata)

self.initialize_wallet_messaging(enable_wallet_messaging)

Expand Down Expand Up @@ -508,6 +514,33 @@ def initialize_wallet_messaging(
else:
self._wallet_messaging_client = None

def _initialize_metadata(
self, metadata: Optional[Dict[str, Any]]
) -> Dict[str, Any]:
"""
Initialize the metadata for the agent.
The metadata is filtered to include only location-based metadata and the
model ensures that the metadata is valid and complete.
Args:
metadata (Optional[Dict[str, Any]]): The metadata to include in the agent object.
Returns:
Dict[str, Any]: The filtered metadata.
"""
if not metadata or "geolocation" not in metadata:
return {}

try:
model = AgentMetadata.model_validate(metadata, strict=True)
filtered_metadata = model.model_dump()
except ValidationError as e:
self._logger.error(f"Invalid metadata: {e}")
filtered_metadata = {}

return filtered_metadata

@property
def name(self) -> str:
"""
Expand Down Expand Up @@ -611,6 +644,16 @@ def balance(self) -> int:

return self.ledger.query_bank_balance(Address(self.wallet.address()))

@property
def metadata(self) -> Dict[str, Any]:
"""
Get the metadata associated with the agent.
Returns:
Dict[str, Any]: The metadata associated with the agent.
"""
return self._metadata

@mailbox.setter
def mailbox(self, config: Union[str, Dict[str, str]]):
"""
Expand Down Expand Up @@ -724,7 +767,7 @@ async def register(self):
)

await self._registration_policy.register(
self.address, list(self.protocols.keys()), self._endpoints
self.address, list(self.protocols.keys()), self._endpoints, self._metadata
)

async def _registration_loop(self):
Expand Down
3 changes: 0 additions & 3 deletions python/src/uagents/envelope.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
from pydantic import (
UUID4,
BaseModel,
ConfigDict,
Field,
field_serializer,
)
Expand Down Expand Up @@ -48,8 +47,6 @@ class Envelope(BaseModel):
nonce: Optional[int] = None
signature: Optional[str] = None

model_config = ConfigDict(populate_by_name=True)

def encode_payload(self, value: JsonStr):
"""
Encode the payload value and store it in the envelope.
Expand Down
53 changes: 39 additions & 14 deletions python/src/uagents/registration.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import json
import logging
from abc import ABC, abstractmethod
from typing import Dict, List, Optional, Union
from typing import Any, Dict, List, Optional

import aiohttp
from cosmpy.aerial.client import LedgerClient
Expand All @@ -23,11 +23,22 @@
from uagents.types import AgentEndpoint


def generate_backoff_time(retry: int) -> float:
"""
Generate a backoff time starting from 0.128 seconds and limited to ~131 seconds
"""
return (2 ** (min(retry, 11) + 6)) / 1000


class AgentRegistrationPolicy(ABC):
@abstractmethod
# pylint: disable=unnecessary-pass
async def register(
self, agent_address: str, protocols: List[str], endpoints: List[AgentEndpoint]
self,
agent_address: str,
protocols: List[str],
endpoints: List[AgentEndpoint],
metadata: Optional[Dict[str, Any]] = None,
):
pass

Expand All @@ -36,7 +47,7 @@ class AgentRegistrationAttestation(BaseModel):
agent_address: str
protocols: List[str]
endpoints: List[AgentEndpoint]
metadata: Optional[Dict[str, Union[str, Dict[str, str]]]] = None
metadata: Optional[Dict[str, Any]] = None
signature: Optional[str] = None

def sign(self, identity: Identity):
Expand Down Expand Up @@ -84,11 +95,18 @@ def __init__(
self._logger = logger or logging.getLogger(__name__)

async def register(
self, agent_address: str, protocols: List[str], endpoints: List[AgentEndpoint]
self,
agent_address: str,
protocols: List[str],
endpoints: List[AgentEndpoint],
metadata: Optional[Dict[str, Any]] = None,
):
# create the attestation
attestation = AgentRegistrationAttestation(
agent_address=agent_address, protocols=protocols, endpoints=endpoints
agent_address=agent_address,
protocols=protocols,
endpoints=endpoints,
metadata=metadata,
)

# sign the attestation
Expand All @@ -102,19 +120,17 @@ async def register(
f"{self._almanac_api}/agents",
headers={"content-type": "application/json"},
data=attestation.model_dump_json(),
timeout=ALMANAC_API_TIMEOUT_SECONDS,
timeout=aiohttp.ClientTimeout(
total=ALMANAC_API_TIMEOUT_SECONDS
),
) as resp:
resp.raise_for_status()
self._logger.info("Registration on Almanac API successful")
return
except (aiohttp.ClientError, asyncio.exceptions.TimeoutError) as e:
if retry == self._max_retries - 1:
raise e

# generate a backoff time starting from 0.128 seconds and limited
# to ~131 seconds
backoff = (2 ** (min(retry, 11) + 6)) / 1000
await asyncio.sleep(backoff)
await asyncio.sleep(generate_backoff_time(retry))


class LedgerBasedRegistrationPolicy(AgentRegistrationPolicy):
Expand All @@ -136,7 +152,11 @@ def __init__(
self._logger = logger or logging.getLogger(__name__)

async def register(
self, agent_address: str, protocols: List[str], endpoints: List[AgentEndpoint]
self,
agent_address: str,
protocols: List[str],
endpoints: List[AgentEndpoint],
metadata: Optional[Dict[str, Any]] = None,
):
# register if not yet registered or registration is about to expire
# or anything has changed from the last registration
Expand Down Expand Up @@ -223,18 +243,23 @@ async def register(
agent_address: str,
protocols: List[str],
endpoints: List[AgentEndpoint],
metadata: Optional[Dict[str, Any]] = None,
):
# prefer the API registration policy as it is faster
try:
await self._api_policy.register(agent_address, protocols, endpoints)
await self._api_policy.register(
agent_address, protocols, endpoints, metadata
)
except Exception as e:
self._logger.warning(
f"Failed to register on Almanac API: {e.__class__.__name__}"
)

# schedule the ledger registration
try:
await self._ledger_policy.register(agent_address, protocols, endpoints)
await self._ledger_policy.register(
agent_address, protocols, endpoints, metadata
)
except InsufficientFundsError:
self._logger.warning(
"Failed to register on Almanac contract due to insufficient funds"
Expand Down
27 changes: 26 additions & 1 deletion python/src/uagents/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from enum import Enum
from typing import (
TYPE_CHECKING,
Annotated,
Any,
Awaitable,
Callable,
Expand All @@ -15,7 +16,7 @@
Union,
)

from pydantic import BaseModel
from pydantic import BaseModel, Field, field_serializer

from uagents.models import Model

Expand Down Expand Up @@ -55,6 +56,30 @@ class RestHandlerDetails(BaseModel):
response_model: Type[Union[Model, BaseModel]]


class AgentGeolocation(BaseModel):
latitude: Annotated[
float,
Field(strict=True, ge=-90, le=90, allow_inf_nan=False),
]
longitude: Annotated[
float,
Field(strict=True, ge=-180, le=180, allow_inf_nan=False),
]
radius: Annotated[
float,
Field(strict=True, ge=0, allow_inf_nan=False),
] = 0

@field_serializer("latitude", "longitude")
def serialize_precision(self, val: float) -> float:
"""Round the latitude and longitude to 6 decimal places."""
return float(f"{val:.6f}")


class AgentMetadata(BaseModel):
geolocation: AgentGeolocation


class DeliveryStatus(str, Enum):
"""Delivery status of a message."""

Expand Down
Loading

0 comments on commit f31be9f

Please sign in to comment.