diff --git a/docs/api/uagents/agent.md b/docs/api/uagents/agent.md new file mode 100644 index 00000000..38880c10 --- /dev/null +++ b/docs/api/uagents/agent.md @@ -0,0 +1,569 @@ + + +# src.uagents.agent + +Agent + + + +## Agent Objects + +```python +class Agent(Sink) +``` + +An agent that interacts within a communication environment. + +**Attributes**: + +- `_name` _str_ - The name of the agent. +- `_port` _int_ - The port on which the agent runs. +- `_background_tasks` _Set[asyncio.Task]_ - Set of background tasks associated with the agent. +- `_resolver` _Resolver_ - The resolver for agent communication. +- `_loop` _asyncio.AbstractEventLoop_ - The asyncio event loop used by the agent. +- `_logger` - The logger instance for logging agent activities. +- `_endpoints` _List[dict]_ - List of communication endpoints. +- `_use_mailbox` _bool_ - Indicates if the agent uses a mailbox for communication. +- `_agentverse` _dict_ - Agentverse configuration settings. +- `_mailbox_client` _MailboxClient_ - Client for interacting with the mailbox. +- `_ledger` - The ledger for recording agent transactions. +- `_almanac_contract` - The almanac contract for agent metadata. +- `_storage` - Key-value store for agent data storage. +- `_interval_handlers` _List[Tuple[IntervalCallback, float]]_ - List of interval + handlers and their periods. +- `_interval_messages` _Set[str]_ - Set of interval message names. +- `_signed_message_handlers` _Dict[str, MessageCallback]_ - Handlers for signed messages. +- `_unsigned_message_handlers` _Dict[str, MessageCallback]_ - Handlers for + unsigned messages. +- `_models` _Dict[str, Type[Model]]_ - Dictionary of supported data models. +- `_replies` _Dict[str, Set[Type[Model]]]_ - Dictionary of reply data models. +- `_queries` _Dict[str, asyncio.Future]_ - Dictionary of active queries. +- `_dispatcher` - The dispatcher for message handling. +- `_message_queue` - Asynchronous queue for incoming messages. +- `_on_startup` _List[Callable]_ - List of functions to run on agent startup. +- `_on_shutdown` _List[Callable]_ - List of functions to run on agent shutdown. +- `_version` _str_ - The version of the agent. +- `_protocol` _Protocol_ - The internal agent protocol. +- `protocols` _Dict[str, Protocol]_ - Dictionary of supported protocols. +- `_ctx` _Context_ - The context for agent interactions. + + +**Methods**: + +- `__init__` - Initialize the Agent instance. + + + +#### `__`init`__` + +```python +def __init__(name: Optional[str] = None, + port: Optional[int] = None, + seed: Optional[str] = None, + endpoint: Optional[Union[str, List[str], Dict[str, dict]]] = None, + agentverse: Optional[Union[str, Dict[str, str]]] = None, + mailbox: Optional[Union[str, Dict[str, str]]] = None, + resolve: Optional[Resolver] = None, + version: Optional[str] = None) +``` + +Initialize an Agent instance. + +**Arguments**: + +- `name` _Optional[str]_ - The name of the agent. +- `port` _Optional[int]_ - The port on which the agent will run. +- `seed` _Optional[str]_ - The seed for generating keys. +- `endpoint` _Optional[Union[str, List[str], Dict[str, dict]]]_ - The endpoint configuration. +- `agentverse` _Optional[Union[str, Dict[str, str]]]_ - The agentverse configuration. +- `mailbox` _Optional[Union[str, Dict[str, str]]]_ - The mailbox configuration. +- `resolve` _Optional[Resolver]_ - The resolver to use for agent communication. +- `version` _Optional[str]_ - The version of the agent. + + + +#### name + +```python +@property +def name() -> str +``` + +Get the name of the agent. + +**Returns**: + +- `str` - The name of the agent. + + + +#### address + +```python +@property +def address() -> str +``` + +Get the address of the agent's identity. + +**Returns**: + +- `str` - The address of the agent's identity. + + + +#### wallet + +```python +@property +def wallet() -> LocalWallet +``` + +Get the wallet of the agent. + +**Returns**: + +- `LocalWallet` - The agent's wallet. + + + +#### storage + +```python +@property +def storage() -> KeyValueStore +``` + +Get the key-value store used by the agent for data storage. + +**Returns**: + +- `KeyValueStore` - The key-value store instance. + + + +#### mailbox + +```python +@property +def mailbox() -> Dict[str, str] +``` + +Get the mailbox configuration of the agent. + +**Returns**: + + Dict[str, str]: The mailbox configuration. + + + +#### agentverse + +```python +@property +def agentverse() -> Dict[str, str] +``` + +Get the agentverse configuration of the agent. + +**Returns**: + + Dict[str, str]: The agentverse configuration. + + + +#### mailbox`_`client + +```python +@property +def mailbox_client() -> MailboxClient +``` + +Get the mailbox client used by the agent for mailbox communication. + +**Returns**: + +- `MailboxClient` - The mailbox client instance. + + + +#### mailbox + +```python +@mailbox.setter +def mailbox(config: Union[str, Dict[str, str]]) +``` + +Set the mailbox configuration for the agent. + +**Arguments**: + +- `config` _Union[str, Dict[str, str]]_ - The new mailbox configuration. + + + +#### agentverse + +```python +@agentverse.setter +def agentverse(config: Union[str, Dict[str, str]]) +``` + +Set the agentverse configuration for the agent. + +**Arguments**: + +- `config` _Union[str, Dict[str, str]]_ - The new agentverse configuration. + + + +#### sign + +```python +def sign(data: bytes) -> str +``` + +Sign the provided data. + +**Arguments**: + +- `data` _bytes_ - The data to be signed. + + +**Returns**: + +- `str` - The signature of the data. + + + +#### sign`_`digest + +```python +def sign_digest(digest: bytes) -> str +``` + +Sign the provided digest. + +**Arguments**: + +- `digest` _bytes_ - The digest to be signed. + + +**Returns**: + +- `str` - The signature of the digest. + + + +#### sign`_`registration + +```python +def sign_registration() -> str +``` + +Sign the registration data for Almanac contract. + +**Returns**: + +- `str` - The signature of the registration data. + + +**Raises**: + +- `AssertionError` - If the Almanac contract address is None. + + + +#### update`_`endpoints + +```python +def update_endpoints(endpoints: List[Dict[str, Any]]) +``` + +Update the list of endpoints. + +**Arguments**: + +- `endpoints` _List[Dict[str, Any]]_ - List of endpoint dictionaries. + + + +#### update`_`loop + +```python +def update_loop(loop) +``` + +Update the event loop. + +**Arguments**: + +- `loop` - The event loop. + + + +#### update`_`queries + +```python +def update_queries(queries) +``` + +Update the queries attribute. + +**Arguments**: + +- `queries` - The queries attribute. + + + +#### register + +```python +async def register() +``` + +Register with the Almanac contract. + +This method checks for registration conditions and performs registration +if necessary. + + + +#### on`_`interval + +```python +def on_interval(period: float, + messages: Optional[Union[Type[Model], + Set[Type[Model]]]] = None) +``` + +Set up an interval event with a callback. + +**Arguments**: + +- `period` _float_ - The interval period. +- `messages` _Optional[Union[Type[Model], Set[Type[Model]]]]_ - Optional message types. + + +**Returns**: + +- `Callable` - The callback function for the interval event. + + + +#### on`_`query + +```python +def on_query(model: Type[Model], + replies: Optional[Union[Model, Set[Model]]] = None) +``` + +Set up a query event with a callback. + +**Arguments**: + +- `model` _Type[Model]_ - The query model. +- `replies` _Optional[Union[Model, Set[Model]]]_ - Optional reply models. + + +**Returns**: + +- `Callable` - The callback function for the query event. + + + +#### on`_`message + +```python +def on_message(model: Type[Model], + replies: Optional[Union[Type[Model], Set[Type[Model]]]] = None, + allow_unverified: Optional[bool] = False) +``` + +Set up a message event with a callback. + +**Arguments**: + +- `model` _Type[Model]_ - The message model. +- `replies` _Optional[Union[Type[Model], Set[Type[Model]]]]_ - Optional reply models. +- `allow_unverified` _Optional[bool]_ - Allow unverified messages. + + +**Returns**: + +- `Callable` - The callback function for the message event. + + + +#### on`_`event + +```python +def on_event(event_type: str) +``` + +Decorator to register an event handler for a specific event type. + +**Arguments**: + +- `event_type` _str_ - The type of event. + + +**Returns**: + +- `Callable` - The decorator function for registering event handlers. + + + +#### include + +```python +def include(protocol: Protocol, publish_manifest: Optional[bool] = False) +``` + +Include a protocol into the agent's capabilities. + +**Arguments**: + +- `protocol` _Protocol_ - The protocol to include. +- `publish_manifest` _Optional[bool]_ - Flag to publish the protocol's manifest. + + +**Raises**: + +- `RuntimeError` - If a duplicate model, signed message handler, or message handler + is encountered. + + + +#### publish`_`manifest + +```python +def publish_manifest(manifest: Dict[str, Any]) +``` + +Publish a protocol manifest to the Almanac. + +**Arguments**: + +- `manifest` _Dict[str, Any]_ - The protocol manifest. + + + +#### handle`_`message + +```python +async def handle_message(sender, schema_digest: str, message: JsonStr, + session: uuid.UUID) +``` + +Handle an incoming message asynchronously. + +**Arguments**: + +- `sender` - The sender of the message. +- `schema_digest` _str_ - The schema digest of the message. +- `message` _JsonStr_ - The message content in JSON format. +- `session` _uuid.UUID_ - The session UUID. + + + +#### setup + +```python +def setup() +``` + +Set up the agent. + + + +#### start`_`background`_`tasks + +```python +def start_background_tasks() +``` + +Start background tasks for the agent. + + + +#### run + +```python +def run() +``` + +Run the agent. + + + +## Bureau Objects + +```python +class Bureau() +``` + +A class representing a Bureau of agents. + +This class manages a collection of agents and orchestrates their execution. + +**Arguments**: + +- `port` _Optional[int]_ - The port number for the server. +- `endpoint` _Optional[Union[str, List[str], Dict[str, dict]]]_ - Configuration + for agent endpoints. + + +**Attributes**: + +- `_loop` _asyncio.AbstractEventLoop_ - The event loop. +- `_agents` _List[Agent]_ - A list of Agent instances within the bureau. +- `_endpoints` _List[Dict[str, Any]]_ - A list of endpoint dictionaries for the agents. +- `_port` _int_ - The port number for the server. +- `_queries` _Dict[str, asyncio.Future]_ - A dictionary of query identifiers to asyncio futures. +- `_logger` _Logger_ - The logger instance. +- `_server` _ASGIServer_ - The ASGI server instance for handling requests. +- `_use_mailbox` _bool_ - A flag indicating whether mailbox functionality is enabled. + + + +#### `__`init`__` + +```python +def __init__(port: Optional[int] = None, + endpoint: Optional[Union[str, List[str], Dict[str, + dict]]] = None) +``` + +Initialize a Bureau instance. + +**Arguments**: + +- `port` _Optional[int]_ - The port number for the server. +- `endpoint` _Optional[Union[str, List[str], Dict[str, dict]]]_ - Configuration + for agent endpoints. + + + +#### add + +```python +def add(agent: Agent) +``` + +Add an agent to the bureau. + +**Arguments**: + +- `agent` _Agent_ - The agent instance to be added. + + + +#### run + +```python +def run() +``` + +Run the agents managed by the bureau. + diff --git a/docs/api/uagents/context.md b/docs/api/uagents/context.md new file mode 100644 index 00000000..0cbac31e --- /dev/null +++ b/docs/api/uagents/context.md @@ -0,0 +1,237 @@ + + +# src.uagents.context + +Agent Context and Message Handling + + + +## MsgDigest Objects + +```python +@dataclass +class MsgDigest() +``` + +Represents a message digest containing a message and its schema digest. + +**Attributes**: + +- `message` _Any_ - The message content. +- `schema_digest` _str_ - The schema digest of the message. + + + +## Context Objects + +```python +class Context() +``` + +Represents the context in which messages are handled and processed. + +**Attributes**: + +- `storage` _KeyValueStore_ - The key-value store for storage operations. +- `wallet` _LocalWallet_ - The local wallet instance for managing identities. +- `ledger` _LedgerClient_ - The ledger client for interacting with distributed ledgers. +- `_name` _Optional[str]_ - The optional name associated with the context. +- `_address` _str_ - The address of the context. +- `_resolver` _Resolver_ - The resolver for name-to-address resolution. +- `_identity` _Identity_ - The identity associated with the context. +- `_queries` _Dict[str, asyncio.Future]_ - Dictionary of query names and their asyncio Futures. +- `_session` _Optional[uuid.UUID]_ - The optional session UUID. +- `_replies` _Optional[Dict[str, Set[Type[Model]]]]_ - The optional dictionary of reply models. +- `_interval_messages` _Optional[Set[str]]_ - The optional set of interval messages. +- `_message_received` _Optional[MsgDigest]_ - The optional message digest received. +- `_protocols` _Optional[Dict[str, Protocol]]_ - The optional dictionary of protocols. +- `_logger` _Optional[logging.Logger]_ - The optional logger instance. + + Properties: +- `name` _str_ - The name associated with the context, or a truncated address if name is None. +- `address` _str_ - The address of the context. +- `logger` _logging.Logger_ - The logger instance. +- `protocols` _Optional[Dict[str, Protocol]]_ - The dictionary of protocols. + + +**Methods**: + +- `get_message_protocol(message_schema_digest)` - Get the protocol associated + with a message schema digest. + send(destination, message, timeout): Send a message to a destination. + + + +#### `__`init`__` + +```python +def __init__(address: str, + name: Optional[str], + storage: KeyValueStore, + resolve: Resolver, + identity: Identity, + wallet: LocalWallet, + ledger: LedgerClient, + queries: Dict[str, asyncio.Future], + session: Optional[uuid.UUID] = None, + replies: Optional[Dict[str, Set[Type[Model]]]] = None, + interval_messages: Optional[Set[str]] = None, + message_received: Optional[MsgDigest] = None, + protocols: Optional[Dict[str, Protocol]] = None, + logger: Optional[logging.Logger] = None) +``` + +Initialize the Context instance. + +**Arguments**: + +- `address` _str_ - The address of the context. +- `name` _Optional[str]_ - The optional name associated with the context. +- `storage` _KeyValueStore_ - The key-value store for storage operations. +- `resolve` _Resolver_ - The resolver for name-to-address resolution. +- `identity` _Identity_ - The identity associated with the context. +- `wallet` _LocalWallet_ - The local wallet instance for managing identities. +- `ledger` _LedgerClient_ - The ledger client for interacting with distributed ledgers. +- `queries` _Dict[str, asyncio.Future]_ - Dictionary of query names and their Futures. +- `session` _Optional[uuid.UUID]_ - The optional session UUID. +- `replies` _Optional[Dict[str, Set[Type[Model]]]]_ - Optional dictionary of reply models. +- `interval_messages` _Optional[Set[str]]_ - The optional set of interval messages. +- `message_received` _Optional[MsgDigest]_ - The optional message digest received. +- `protocols` _Optional[Dict[str, Protocol]]_ - The optional dictionary of protocols. +- `logger` _Optional[logging.Logger]_ - The optional logger instance. + + + +#### name + +```python +@property +def name() -> str +``` + +Get the name associated with the context or a truncated address if name is None. + +**Returns**: + +- `str` - The name or truncated address. + + + +#### address + +```python +@property +def address() -> str +``` + +Get the address of the context. + +**Returns**: + +- `str` - The address of the context. + + + +#### logger + +```python +@property +def logger() -> logging.Logger +``` + +Get the logger instance associated with the context. + +**Returns**: + +- `logging.Logger` - The logger instance. + + + +#### protocols + +```python +@property +def protocols() -> Optional[Dict[str, Protocol]] +``` + +Get the dictionary of protocols associated with the context. + +**Returns**: + + Optional[Dict[str, Protocol]]: The dictionary of protocols. + + + +#### session + +```python +@property +def session() -> uuid.UUID +``` + +Get the session UUID associated with the context. + +**Returns**: + +- `uuid.UUID` - The session UUID. + + + +#### get`_`message`_`protocol + +```python +def get_message_protocol(message_schema_digest) -> Optional[str] +``` + +Get the protocol associated with a given message schema digest. + +**Arguments**: + +- `message_schema_digest` _str_ - The schema digest of the message. + + +**Returns**: + +- `Optional[str]` - The protocol digest associated with the message schema digest, + or None if not found. + + + +#### send + +```python +async def send(destination: str, + message: Model, + timeout: Optional[int] = DEFAULT_ENVELOPE_TIMEOUT_SECONDS) +``` + +Send a message to the specified destination. + +**Arguments**: + +- `destination` _str_ - The destination address to send the message to. +- `message` _Model_ - The message to be sent. +- `timeout` _Optional[int]_ - The optional timeout for sending the message, in seconds. + + + +#### send`_`raw + +```python +async def send_raw(destination: str, + json_message: JsonStr, + schema_digest: str, + message_type: Optional[Type[Model]] = None, + timeout: Optional[int] = DEFAULT_ENVELOPE_TIMEOUT_SECONDS) +``` + +Send a raw message to the specified destination. + +**Arguments**: + +- `destination` _str_ - The destination address to send the message to. +- `json_message` _JsonStr_ - The JSON-encoded message to be sent. +- `schema_digest` _str_ - The schema digest of the message. +- `message_type` _Optional[Type[Model]]_ - The optional type of the message being sent. +- `timeout` _Optional[int]_ - The optional timeout for sending the message, in seconds. + diff --git a/docs/api/uagents/envelope.md b/docs/api/uagents/envelope.md new file mode 100644 index 00000000..4f393939 --- /dev/null +++ b/docs/api/uagents/envelope.md @@ -0,0 +1,85 @@ + + +# src.uagents.envelope + +Agent Envelope. + + + +## Envelope Objects + +```python +class Envelope(BaseModel) +``` + +Represents an envelope for message communication between agents. + +**Attributes**: + +- `version` _int_ - The envelope version. +- `sender` _str_ - The sender's address. +- `target` _str_ - The target's address. +- `session` _UUID4_ - The session UUID. +- `schema_digest` _str_ - The schema digest (alias for protocol). +- `protocol_digest` _Optional[str]_ - The protocol digest (optional). +- `payload` _Optional[str]_ - The payload data (optional). +- `expires` _Optional[int]_ - The expiration timestamp (optional). +- `nonce` _Optional[int]_ - The nonce value (optional). +- `signature` _Optional[str]_ - The envelope signature (optional). + + + +#### encode`_`payload + +```python +def encode_payload(value: JsonStr) +``` + +Encode the payload value and store it in the envelope. + +**Arguments**: + +- `value` _JsonStr_ - The payload value to be encoded. + + + +#### decode`_`payload + +```python +def decode_payload() -> Optional[Any] +``` + +Decode and retrieve the payload value from the envelope. + +**Returns**: + +- `Optional[Any]` - The decoded payload value, or None if payload is not present. + + + +#### sign + +```python +def sign(identity: Identity) +``` + +Sign the envelope using the provided identity. + +**Arguments**: + +- `identity` _Identity_ - The identity used for signing. + + + +#### verify + +```python +def verify() -> bool +``` + +Verify the envelope's signature. + +**Returns**: + +- `bool` - True if the signature is valid, False otherwise. + diff --git a/docs/api/uagents/network.md b/docs/api/uagents/network.md new file mode 100644 index 00000000..255076ce --- /dev/null +++ b/docs/api/uagents/network.md @@ -0,0 +1,353 @@ + + +# src.uagents.network + +Network and Contracts. + + + +#### get`_`ledger + +```python +def get_ledger() -> LedgerClient +``` + +Get the Ledger client. + +**Returns**: + +- `LedgerClient` - The Ledger client instance. + + + +#### get`_`faucet + +```python +def get_faucet() -> FaucetApi +``` + +Get the Faucet API instance. + +**Returns**: + +- `FaucetApi` - The Faucet API instance. + + + +#### wait`_`for`_`tx`_`to`_`complete + +```python +async def wait_for_tx_to_complete( + tx_hash: str, + timeout: Optional[timedelta] = None, + poll_period: Optional[timedelta] = None) -> TxResponse +``` + +Wait for a transaction to complete on the Ledger. + +**Arguments**: + +- `tx_hash` _str_ - The hash of the transaction to monitor. +- `timeout` _Optional[timedelta], optional_ - The maximum time to wait for + the transaction to complete. Defaults to None. +- `poll_period` _Optional[timedelta], optional_ - The time interval to poll + the Ledger for the transaction status. Defaults to None. + + +**Returns**: + +- `TxResponse` - The response object containing the transaction details. + + + +## AlmanacContract Objects + +```python +class AlmanacContract(LedgerContract) +``` + +A class representing the Almanac contract for agent registration. + +This class provides methods to interact with the Almanac contract, including +checking if an agent is registered, retrieving the expiry height of an agent's +registration, and getting the endpoints associated with an agent's registration. + +**Arguments**: + + ledger contract (LedgerContract): An instance of the LedgeContract class. + + +**Attributes**: + + ledger contract (LedgerContract): An instance of the LedgeContract class. + + + +#### is`_`registered + +```python +def is_registered(address: str) -> bool +``` + +Check if an agent is registered in the Almanac contract. + +**Arguments**: + +- `address` _str_ - The agent's address. + + +**Returns**: + +- `bool` - True if the agent is registered, False otherwise. + + + +#### get`_`expiry + +```python +def get_expiry(address: str) -> int +``` + +Get the expiry height of an agent's registration. + +**Arguments**: + +- `address` _str_ - The agent's address. + + +**Returns**: + +- `int` - The expiry height of the agent's registration. + + + +#### get`_`endpoints + +```python +def get_endpoints(address: str) +``` + +Get the endpoints associated with an agent's registration. + +**Arguments**: + +- `address` _str_ - The agent's address. + + +**Returns**: + +- `Any` - The endpoints associated with the agent's registration. + + + +#### get`_`protocols + +```python +def get_protocols(address: str) +``` + +Get the protocols associated with an agent's registration. + +**Arguments**: + +- `address` _str_ - The agent's address. + + +**Returns**: + +- `Any` - The protocols associated with the agent's registration. + + + +#### register + +```python +async def register(ledger: LedgerClient, wallet: LocalWallet, + agent_address: str, protocols: List[str], + endpoints: List[Dict[str, Any]], signature: str) +``` + +Register an agent with the Almanac contract. + +**Arguments**: + +- `ledger` _LedgerClient_ - The Ledger client. +- `wallet` _LocalWallet_ - The agent's wallet. +- `agent_address` _str_ - The agent's address. +- `protocols` _List[str]_ - List of protocols. +- `endpoints` _List[Dict[str, Any]]_ - List of endpoint dictionaries. +- `signature` _str_ - The agent's signature. + + + +#### get`_`sequence + +```python +def get_sequence(address: str) -> int +``` + +Get the agent's sequence number. + +**Arguments**: + +- `address` _str_ - The agent's address. + + +**Returns**: + +- `int` - The agent's sequence number. + + + +#### get`_`almanac`_`contract + +```python +def get_almanac_contract() -> AlmanacContract +``` + +Get the AlmanacContract instance. + +**Returns**: + +- `AlmanacContract` - The AlmanacContract instance. + + + +## NameServiceContract Objects + +```python +class NameServiceContract(LedgerContract) +``` + +A class representing the NameService contract for managing domain names and ownership. + +This class provides methods to interact with the NameService contract, including +checking name availability, checking ownership, querying domain public status, +obtaining registration transaction details, and registering a name within a domain. + +**Arguments**: + + ledger contract (LedgerContract): An instance of the LedgeContract class. + + +**Attributes**: + + ledger contract (LedgerContract): An instance of the LedgeContract class. + + + +#### is`_`name`_`available + +```python +def is_name_available(name: str, domain: str) +``` + +Check if a name is available within a domain. + +**Arguments**: + +- `name` _str_ - The name to check. +- `domain` _str_ - The domain to check within. + + +**Returns**: + +- `bool` - True if the name is available, False otherwise. + + + +#### is`_`owner + +```python +def is_owner(name: str, domain: str, wallet_address: str) +``` + +Check if the provided wallet address is the owner of a name within a domain. + +**Arguments**: + +- `name` _str_ - The name to check ownership for. +- `domain` _str_ - The domain to check within. +- `wallet_address` _str_ - The wallet address to check ownership against. + + +**Returns**: + +- `bool` - True if the wallet address is the owner, False otherwise. + + + +#### is`_`domain`_`public + +```python +def is_domain_public(domain: str) +``` + +Check if a domain is public. + +**Arguments**: + +- `domain` _str_ - The domain to check. + + +**Returns**: + +- `bool` - True if the domain is public, False otherwise. + + + +#### get`_`registration`_`tx + +```python +def get_registration_tx(name: str, wallet_address: str, agent_address: str, + domain: str) +``` + +Get the registration transaction for registering a name within a domain. + +**Arguments**: + +- `name` _str_ - The name to be registered. +- `wallet_address` _str_ - The wallet address initiating the registration. +- `agent_address` _str_ - The address of the agent. +- `domain` _str_ - The domain in which the name is registered. + + +**Returns**: + +- `Optional[Transaction]` - The registration transaction, or None if the name is not + available or not owned by the wallet address. + + + +#### register + +```python +async def register(ledger: LedgerClient, wallet: LocalWallet, + agent_address: str, name: str, domain: str) +``` + +Register a name within a domain using the NameService contract. + +**Arguments**: + +- `ledger` _LedgerClient_ - The Ledger client. +- `wallet` _LocalWallet_ - The wallet of the agent. +- `agent_address` _str_ - The address of the agent. +- `name` _str_ - The name to be registered. +- `domain` _str_ - The domain in which the name is registered. + + + +#### get`_`name`_`service`_`contract + +```python +def get_name_service_contract() -> NameServiceContract +``` + +Get the NameServiceContract instance. + +**Returns**: + +- `NameServiceContract` - The NameServiceContract instance. + diff --git a/docs/api/uagents/protocol.md b/docs/api/uagents/protocol.md new file mode 100644 index 00000000..fd170eab --- /dev/null +++ b/docs/api/uagents/protocol.md @@ -0,0 +1,286 @@ + + +# src.uagents.protocol + +Exchange Protocol + + + +## Protocol Objects + +```python +class Protocol() +``` + + + +#### `__`init`__` + +```python +def __init__(name: Optional[str] = None, version: Optional[str] = None) +``` + +Initialize a Protocol instance. + +**Arguments**: + +- `name` _Optional[str], optional_ - The name of the protocol. Defaults to None. +- `version` _Optional[str], optional_ - The version of the protocol. Defaults to None. + + + +#### intervals + +```python +@property +def intervals() +``` + +Property to access the interval handlers. + +**Returns**: + + List[Tuple[IntervalCallback, float]]: List of interval handlers and their periods. + + + +#### models + +```python +@property +def models() +``` + +Property to access the registered models. + +**Returns**: + + Dict[str, Type[Model]]: Dictionary of registered models with schema digests as keys. + + + +#### replies + +```python +@property +def replies() +``` + +Property to access the registered replies. + +**Returns**: + + Dict[str, Dict[str, Type[Model]]]: Dictionary of registered replies with request + schema digests as keys. + + + +#### interval`_`messages + +```python +@property +def interval_messages() +``` + +Property to access the interval message digests. + +**Returns**: + +- `Set[str]` - Set of message digests associated with interval messages. + + + +#### signed`_`message`_`handlers + +```python +@property +def signed_message_handlers() +``` + +Property to access the signed message handlers. + +**Returns**: + + Dict[str, MessageCallback]: Dictionary of signed message handlers with message schema + digests as keys. + + + +#### unsigned`_`message`_`handlers + +```python +@property +def unsigned_message_handlers() +``` + +Property to access the unsigned message handlers. + +**Returns**: + + Dict[str, MessageCallback]: Dictionary of unsigned message handlers with message schema + digests as keys. + + + +#### name + +```python +@property +def name() +``` + +Property to access the protocol name. + +**Returns**: + +- `str` - The protocol name. + + + +#### version + +```python +@property +def version() +``` + +Property to access the protocol version. + +**Returns**: + +- `str` - The protocol version. + + + +#### canonical`_`name + +```python +@property +def canonical_name() +``` + +Property to access the canonical name of the protocol. + +**Returns**: + +- `str` - The canonical name of the protocol. + + + +#### digest + +```python +@property +def digest() +``` + +Property to access the digest of the protocol's manifest. + +**Returns**: + +- `str` - The digest of the protocol's manifest. + + + +#### on`_`interval + +```python +def on_interval(period: float, + messages: Optional[Union[Type[Model], + Set[Type[Model]]]] = None) +``` + +Decorator to register an interval handler for the protocol. + +**Arguments**: + +- `period` _float_ - The interval period in seconds. +- `messages` _Optional[Union[Type[Model], Set[Type[Model]]]], optional_ - The associated + message types. Defaults to None. + + +**Returns**: + +- `Callable` - The decorator to register the interval handler. + + + +#### on`_`query + +```python +def on_query(model: Type[Model], + replies: Optional[Union[Type[Model], Set[Type[Model]]]] = None) +``` + +Decorator to register a query handler for the protocol. + +**Arguments**: + +- `model` _Type[Model]_ - The message model type. +- `replies` _Optional[Union[Type[Model], Set[Type[Model]]]], optional_ - The associated + reply types. Defaults to None. + + +**Returns**: + +- `Callable` - The decorator to register the query handler. + + + +#### on`_`message + +```python +def on_message(model: Type[Model], + replies: Optional[Union[Type[Model], Set[Type[Model]]]] = None, + allow_unverified: Optional[bool] = False) +``` + +Decorator to register a message handler for the protocol. + +**Arguments**: + +- `model` _Type[Model]_ - The message model type. +- `replies` _Optional[Union[Type[Model], Set[Type[Model]]]], optional_ - The associated + reply types. Defaults to None. +- `allow_unverified` _Optional[bool], optional_ - Whether to allow unverified messages. + Defaults to False. + + +**Returns**: + +- `Callable` - The decorator to register the message handler. + + + +#### manifest + +```python +def manifest() -> Dict[str, Any] +``` + +Generate the protocol's manifest. + +**Returns**: + + Dict[str, Any]: The protocol's manifest. + + + +#### compute`_`digest + +```python +@staticmethod +def compute_digest(manifest: Dict[str, Any]) -> str +``` + +Compute the digest of a given manifest. + +**Arguments**: + +- `manifest` _Dict[str, Any]_ - The manifest to compute the digest for. + + +**Returns**: + +- `str` - The computed digest. + diff --git a/docs/api/uagents/query.md b/docs/api/uagents/query.md new file mode 100644 index 00000000..1238b9f0 --- /dev/null +++ b/docs/api/uagents/query.md @@ -0,0 +1,76 @@ + + +# src.uagents.query + +Query Envelopes. + + + +#### query + +```python +async def query(destination: str, + message: Model, + resolver: Optional[Resolver] = None, + timeout: Optional[int] = 30) -> Optional[Envelope] +``` + +Query a remote agent with a message and retrieve the response envelope. + +**Arguments**: + +- `destination` _str_ - The destination address of the remote agent. +- `message` _Model_ - The message to send. +- `resolver` _Optional[Resolver], optional_ - The resolver to use for endpoint resolution. + Defaults to GlobalResolver. +- `timeout` _Optional[int], optional_ - The timeout for the query in seconds. Defaults to 30. + + +**Returns**: + +- `Optional[Envelope]` - The response envelope if successful, otherwise None. + + + +#### enclose`_`response + +```python +def enclose_response(message: Model, sender: str, session: str) -> str +``` + +Enclose a response message within an envelope. + +**Arguments**: + +- `message` _Model_ - The response message to enclose. +- `sender` _str_ - The sender's address. +- `session` _str_ - The session identifier. + + +**Returns**: + +- `str` - The JSON representation of the response envelope. + + + +#### enclose`_`response`_`raw + +```python +def enclose_response_raw(json_message: JsonStr, schema_digest: str, + sender: str, session: str) -> str +``` + +Enclose a raw response message within an envelope. + +**Arguments**: + +- `json_message` _JsonStr_ - The JSON-formatted response message to enclose. +- `schema_digest` _str_ - The schema digest of the message. +- `sender` _str_ - The sender's address. +- `session` _str_ - The session identifier. + + +**Returns**: + +- `str` - The JSON representation of the response envelope. + diff --git a/docs/api/uagents/resolver.md b/docs/api/uagents/resolver.md new file mode 100644 index 00000000..57191638 --- /dev/null +++ b/docs/api/uagents/resolver.md @@ -0,0 +1,214 @@ + + +# src.uagents.resolver + +Endpoint Resolver. + + + +#### query`_`record + +```python +def query_record(agent_address: str, service: str) -> dict +``` + +Query a record from the Almanac contract. + +**Arguments**: + +- `agent_address` _str_ - The address of the agent. +- `service` _str_ - The type of service to query. + + +**Returns**: + +- `dict` - The query result. + + + +#### get`_`agent`_`address + +```python +def get_agent_address(name: str) -> str +``` + +Get the agent address associated with the provided name from the name service contract. + +**Arguments**: + +- `name` _str_ - The name to query. + + +**Returns**: + +- `str` - The associated agent address. + + + +#### is`_`agent`_`address + +```python +def is_agent_address(address) +``` + +Check if the provided address is a valid agent address. + +**Arguments**: + +- `address` - The address to check. + + +**Returns**: + +- `bool` - True if the address is a valid agent address, False otherwise. + + + +## Resolver Objects + +```python +class Resolver(ABC) +``` + + + +#### resolve + +```python +@abstractmethod +async def resolve(destination: str) -> Optional[str] +``` + +Resolve the destination to an endpoint. + +**Arguments**: + +- `destination` _str_ - The destination to resolve. + + +**Returns**: + +- `Optional[str]` - The resolved endpoint or None. + + + +## GlobalResolver Objects + +```python +class GlobalResolver(Resolver) +``` + + + +#### resolve + +```python +async def resolve(destination: str) -> Optional[str] +``` + +Resolve the destination using a combination of Almanac and NameService resolvers. + +**Arguments**: + +- `destination` _str_ - The destination to resolve. + + +**Returns**: + +- `Optional[str]` - The resolved endpoint or None. + + + +## AlmanacResolver Objects + +```python +class AlmanacResolver(Resolver) +``` + + + +#### resolve + +```python +async def resolve(destination: str) -> Optional[str] +``` + +Resolve the destination using the Almanac contract. + +**Arguments**: + +- `destination` _str_ - The destination to resolve. + + +**Returns**: + +- `Optional[str]` - The resolved endpoint or None. + + + +## NameServiceResolver Objects + +```python +class NameServiceResolver(Resolver) +``` + + + +#### resolve + +```python +async def resolve(destination: str) -> Optional[str] +``` + +Resolve the destination using the NameService contract. + +**Arguments**: + +- `destination` _str_ - The destination to resolve. + + +**Returns**: + +- `Optional[str]` - The resolved endpoint or None. + + + +## RulesBasedResolver Objects + +```python +class RulesBasedResolver(Resolver) +``` + + + +#### `__`init`__` + +```python +def __init__(rules: Dict[str, str]) +``` + +Initialize the RulesBasedResolver with the provided rules. + +**Arguments**: + +- `rules` _Dict[str, str]_ - A dictionary of rules mapping destinations to endpoints. + + + +#### resolve + +```python +async def resolve(destination: str) -> Optional[str] +``` + +Resolve the destination using the provided rules. + +**Arguments**: + +- `destination` _str_ - The destination to resolve. + + +**Returns**: + +- `Optional[str]` - The resolved endpoint or None. + diff --git a/docs/api/uagents/setup.md b/docs/api/uagents/setup.md new file mode 100644 index 00000000..5d755b65 --- /dev/null +++ b/docs/api/uagents/setup.md @@ -0,0 +1,45 @@ + + +# src.uagents.setup + +Agent's Setup. + + + +#### fund`_`agent`_`if`_`low + +```python +def fund_agent_if_low(wallet_address: str) +``` + +Checks the agent's wallet balance and adds funds if it's below the registration fee. + +**Arguments**: + +- `wallet_address` _str_ - The wallet address of the agent. + + +**Returns**: + + None + + + +#### register`_`agent`_`with`_`mailbox + +```python +def register_agent_with_mailbox(agent: Agent, email: str) +``` + +Registers the agent on a mailbox server using the provided email. + +**Arguments**: + +- `agent` _Agent_ - The agent object to be registered. +- `email` _str_ - The email address associated with the agent. + + +**Returns**: + + None + diff --git a/docs/api/uagents/storage/__init__.md b/docs/api/uagents/storage/__init__.md new file mode 100644 index 00000000..92a6a77d --- /dev/null +++ b/docs/api/uagents/storage/__init__.md @@ -0,0 +1,96 @@ + + +# src.uagents.storage.`__`init`__` + + + +## KeyValueStore Objects + +```python +class KeyValueStore() +``` + +A simple key-value store implementation for data storage. + +**Attributes**: + +- `_data` _dict_ - The internal data storage dictionary. +- `_name` _str_ - The name associated with the store. +- `_path` _str_ - The file path where the store data is stored. + + +**Methods**: + +- `__init__` - Initialize the KeyValueStore instance. +- `get` - Get the value associated with a key from the store. +- `has` - Check if a key exists in the store. +- `set` - Set a value associated with a key in the store. +- `remove` - Remove a key and its associated value from the store. +- `clear` - Clear all data from the store. +- `_load` - Load data from the file into the store. +- `_save` - Save the store data to the file. + + + +#### `__`init`__` + +```python +def __init__(name: str, cwd: str = None) +``` + +Initialize the KeyValueStore instance. + +**Arguments**: + +- `name` _str_ - The name associated with the store. +- `cwd` _str, optional_ - The current working directory. Defaults to None. + + + +#### load`_`all`_`keys + +```python +def load_all_keys() -> dict +``` + +Load all private keys from the private keys file. + +**Returns**: + +- `dict` - A dictionary containing loaded private keys. + + + +#### save`_`private`_`keys + +```python +def save_private_keys(name: str, identity_key: str, wallet_key: str) +``` + +Save private keys to the private keys file. + +**Arguments**: + +- `name` _str_ - The name associated with the private keys. +- `identity_key` _str_ - The identity private key. +- `wallet_key` _str_ - The wallet private key. + + + +#### get`_`or`_`create`_`private`_`keys + +```python +def get_or_create_private_keys(name: str) -> Tuple[str, str] +``` + +Get or create private keys associated with a name. + +**Arguments**: + +- `name` _str_ - The name associated with the private keys. + + +**Returns**: + + Tuple[str, str]: A tuple containing the identity key and wallet key. + diff --git a/generate_api_docs.py b/generate_api_docs.py new file mode 100644 index 00000000..22921387 --- /dev/null +++ b/generate_api_docs.py @@ -0,0 +1,166 @@ +"""This tool generates the API docs.""" +import argparse +import re +import shutil +import subprocess # nosec +import sys +from pathlib import Path + + +DOCS_DIR = Path("docs/") +API_DIR = DOCS_DIR / "api/" +UAGENTS_DIR = Path("src/uagents") + +IGNORE_NAMES = {} +IGNORE_PREFIXES = { + Path(UAGENTS_DIR, "__init__.py"), + Path(UAGENTS_DIR, "asgi.py"), + Path(UAGENTS_DIR, "config.py"), + Path(UAGENTS_DIR, "contrib"), + Path(UAGENTS_DIR, "crypto"), + Path(UAGENTS_DIR, "dispatch.py"), + Path(UAGENTS_DIR, "mailbox.py"), + Path(UAGENTS_DIR, "models.py"), +} + + +def create_subdir(path: str) -> None: + """ + Create a subdirectory. + + :param path: the directory path + """ + directory = "/".join(path.split("/")[:-1]) + Path(directory).mkdir(parents=True, exist_ok=True) + + +def replace_underscores(text: str) -> str: + """ + Replace escaped underscores in a text. + + :param text: the text to replace underscores in + :return: the processed text + """ + text_a = text.replace("\\_\\_", "`__`") + text_b = text_a.replace("\\_", "`_`") + return text_b + + +def is_relative_to(path_1: Path, path_2: Path) -> bool: + """Check if a path is relative to another path.""" + return str(path_1).startswith(str(path_2)) + + +def is_not_dir(path: Path) -> bool: + """Call p.is_dir() method and negate the result.""" + return not path.is_dir() + + +def should_skip(module_path: Path) -> bool: + """Return true if the file should be skipped.""" + if any(re.search(pattern, module_path.name) for pattern in IGNORE_NAMES): + print("Skipping, it's in ignore patterns") + return True + if module_path.suffix != ".py": + print("Skipping, it's not a Python module.") + return True + if any(is_relative_to(module_path, prefix) for prefix in IGNORE_PREFIXES): + print(f"Ignoring prefix {module_path}") + return True + return False + + +def _generate_apidocs_uagents_modules() -> None: + """Generate API docs for uagents.* modules.""" + for module_path in filter(is_not_dir, Path(UAGENTS_DIR).rglob("*")): + print(f"Processing {module_path}... ", end="") + if should_skip(module_path): + continue + parents = module_path.parts[:-1] + parents_without_root = module_path.parts[1:-1] + last = module_path.stem + doc_file = API_DIR / Path(*parents_without_root) / f"{last}.md" + dotted_path = ".".join(parents) + "." + last + make_pydoc(dotted_path, doc_file) + + +def make_pydoc(dotted_path: str, destination_file: Path) -> None: + """Make a PyDoc file.""" + print( + f"Running with dotted path={dotted_path} and destination_file={destination_file}... ", + end="", + ) + try: + api_doc_content = run_pydoc_markdown(dotted_path) + destination_file.parent.mkdir(parents=True, exist_ok=True) + destination_file.write_text(api_doc_content) + except Exception as ex: # pylint: disable=broad-except + print(f"Error: {str(ex)}") + return + print("Done!") + + +def run_pydoc_markdown(module: str) -> str: + """ + Run pydoc-markdown. + + :param module: the dotted path. + :return: the PyDoc content (pre-processed). + """ + with subprocess.Popen( + ["pydoc-markdown", "-m", module, "-I", "."], stdout=subprocess.PIPE + ) as pydoc: + stdout, _ = pydoc.communicate() + pydoc.wait() + stdout_text = stdout.decode("utf-8") + text = replace_underscores(stdout_text) + return text + + +def generate_api_docs() -> None: + """Generate the api docs.""" + shutil.rmtree(API_DIR, ignore_errors=True) + API_DIR.mkdir() + _generate_apidocs_uagents_modules() + + +def install(package: str) -> int: + """ + Install a PyPI package by calling pip. + + :param package: the package name and version specifier. + :return: the return code. + """ + return subprocess.check_call( # nosec + [sys.executable, "-m", "pip", "install", package] + ) + + +def check_working_tree_is_dirty() -> None: + """Check if the current Git working tree is dirty.""" + print("Checking whether the Git working tree is dirty...") + result = subprocess.check_output(["git", "diff", "--stat"]) # nosec + if len(result) > 0: + print("Git working tree is dirty:") + print(result.decode("utf-8")) + sys.exit(1) + else: + print("All good!") + + +if __name__ == "__main__": + parser = argparse.ArgumentParser("generate_api_docs") + parser.add_argument( + "--check-clean", action="store_true", help="Check if the working tree is clean." + ) + arguments = parser.parse_args() + + res = shutil.which("pydoc-markdown") + if res is None: + install("pydoc-markdown") + sys.exit(1) + + generate_api_docs() + + if arguments.check_clean: + check_working_tree_is_dirty() diff --git a/mkdocs.yml b/mkdocs.yml index f29c4a5e..c7837814 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -25,6 +25,17 @@ nav: - Endpoint weighting: almanac-endpoint.md - Exchange protocol: 'protocol.md' # - Marketplace contract: + - API: + - Agent: 'api/uagents/agent.md' + - Context: 'api/uagents/context.md' + - Envelope: 'api/uagents/envelope.md' + - Network and Contracts: 'api/uagents/network.md' + - Protocol: 'api/uagents/protocol.md' + - Envelope query: 'api/uagents/query.md' + - Endpoint resolver: 'api/uagents/resolver.md' + - Agent setup: 'api/uagents/setup.md' + - Storage: + - Storage functionality: 'api/uagents/storage/__init__.md' theme: name: material diff --git a/src/uagents/agent.py b/src/uagents/agent.py index 9a58e174..95d7f600 100644 --- a/src/uagents/agent.py +++ b/src/uagents/agent.py @@ -1,3 +1,5 @@ +"""Agent""" + import asyncio import functools from typing import Dict, List, Optional, Set, Union, Type, Tuple, Any, Coroutine @@ -28,8 +30,9 @@ from uagents.mailbox import MailboxClient from uagents.config import ( REGISTRATION_FEE, - MIN_REGISTRATION_TIME, + REGISTRATION_UPDATE_INTERVAL_SECONDS, LEDGER_PREFIX, + REGISTRATION_RETRY_INTERVAL_SECONDS, parse_endpoint_config, parse_agentverse_config, get_logger, @@ -37,6 +40,14 @@ async def _run_interval(func: IntervalCallback, ctx: Context, period: float): + """ + Run the provided interval callback function at a specified period. + + Args: + func (IntervalCallback): The interval callback function to run. + ctx (Context): The context for the agent. + period (float): The time period at which to run the callback function. + """ while True: try: await func(ctx) @@ -49,15 +60,69 @@ async def _run_interval(func: IntervalCallback, ctx: Context, period: float): async def _delay(coroutine: Coroutine, delay_seconds: float): + """ + Delay the execution of the provided coroutine by the specified number of seconds. + + Args: + coroutine (Coroutine): The coroutine to delay. + delay_seconds (float): The delay time in seconds. + """ await asyncio.sleep(delay_seconds) await coroutine async def _handle_error(ctx: Context, destination: str, msg: ErrorMessage): + """ + Handle an error message by sending it to the specified destination. + + Args: + ctx (Context): The context for the agent. + destination (str): The destination address to send the error message to. + msg (ErrorMessage): The error message to handle. + """ await ctx.send(destination, msg) class Agent(Sink): + """ + An agent that interacts within a communication environment. + + Attributes: + _name (str): The name of the agent. + _port (int): The port on which the agent runs. + _background_tasks (Set[asyncio.Task]): Set of background tasks associated with the agent. + _resolver (Resolver): The resolver for agent communication. + _loop (asyncio.AbstractEventLoop): The asyncio event loop used by the agent. + _logger: The logger instance for logging agent activities. + _endpoints (List[dict]): List of communication endpoints. + _use_mailbox (bool): Indicates if the agent uses a mailbox for communication. + _agentverse (dict): Agentverse configuration settings. + _mailbox_client (MailboxClient): Client for interacting with the mailbox. + _ledger: The ledger for recording agent transactions. + _almanac_contract: The almanac contract for agent metadata. + _storage: Key-value store for agent data storage. + _interval_handlers (List[Tuple[IntervalCallback, float]]): List of interval + handlers and their periods. + _interval_messages (Set[str]): Set of interval message names. + _signed_message_handlers (Dict[str, MessageCallback]): Handlers for signed messages. + _unsigned_message_handlers (Dict[str, MessageCallback]): Handlers for + unsigned messages. + _models (Dict[str, Type[Model]]): Dictionary of supported data models. + _replies (Dict[str, Set[Type[Model]]]): Dictionary of reply data models. + _queries (Dict[str, asyncio.Future]): Dictionary of active queries. + _dispatcher: The dispatcher for message handling. + _message_queue: Asynchronous queue for incoming messages. + _on_startup (List[Callable]): List of functions to run on agent startup. + _on_shutdown (List[Callable]): List of functions to run on agent shutdown. + _version (str): The version of the agent. + _protocol (Protocol): The internal agent protocol. + protocols (Dict[str, Protocol]): Dictionary of supported protocols. + _ctx (Context): The context for agent interactions. + + Methods: + __init__: Initialize the Agent instance. + """ + def __init__( self, name: Optional[str] = None, @@ -69,6 +134,19 @@ def __init__( resolve: Optional[Resolver] = None, version: Optional[str] = None, ): + """ + Initialize an Agent instance. + + Args: + name (Optional[str]): The name of the agent. + port (Optional[int]): The port on which the agent will run. + seed (Optional[str]): The seed for generating keys. + endpoint (Optional[Union[str, List[str], Dict[str, dict]]]): The endpoint configuration. + agentverse (Optional[Union[str, Dict[str, str]]]): The agentverse configuration. + mailbox (Optional[Union[str, Dict[str, str]]]): The mailbox configuration. + resolve (Optional[Resolver]): The resolver to use for agent communication. + version (Optional[str]): The version of the agent. + """ self._name = name self._port = port if port is not None else 8000 self._background_tasks: Set[asyncio.Task] = set() @@ -155,6 +233,16 @@ def __init__( ) def _initialize_wallet_and_identity(self, seed, name): + """ + Initialize the wallet and identity for the agent. + + If seed is provided, the identity and wallet are derived from the seed. + If seed is not provided, they are either generated or fetched based on the provided name. + + Args: + seed (str or None): The seed for generating keys. + name (str or None): The name of the agent. + """ if seed is None: if name is None: self._wallet = LocalWallet.generate() @@ -174,47 +262,131 @@ def _initialize_wallet_and_identity(self, seed, name): @property def name(self) -> str: + """ + Get the name of the agent. + + Returns: + str: The name of the agent. + """ return self._name @property def address(self) -> str: + """ + Get the address of the agent's identity. + + Returns: + str: The address of the agent's identity. + """ return self._identity.address @property def wallet(self) -> LocalWallet: + """ + Get the wallet of the agent. + + Returns: + LocalWallet: The agent's wallet. + """ return self._wallet @property def storage(self) -> KeyValueStore: + """ + Get the key-value store used by the agent for data storage. + + Returns: + KeyValueStore: The key-value store instance. + """ return self._storage @property def mailbox(self) -> Dict[str, str]: + """ + Get the mailbox configuration of the agent. + + Returns: + Dict[str, str]: The mailbox configuration. + """ return self._agentverse @property def agentverse(self) -> Dict[str, str]: + """ + Get the agentverse configuration of the agent. + + Returns: + Dict[str, str]: The agentverse configuration. + """ return self._agentverse @property def mailbox_client(self) -> MailboxClient: + """ + Get the mailbox client used by the agent for mailbox communication. + + Returns: + MailboxClient: The mailbox client instance. + """ return self._mailbox_client @mailbox.setter def mailbox(self, config: Union[str, Dict[str, str]]): + """ + Set the mailbox configuration for the agent. + + Args: + config (Union[str, Dict[str, str]]): The new mailbox configuration. + """ self._agentverse = parse_agentverse_config(config) @agentverse.setter def agentverse(self, config: Union[str, Dict[str, str]]): + """ + Set the agentverse configuration for the agent. + + Args: + config (Union[str, Dict[str, str]]): The new agentverse configuration. + """ self._agentverse = parse_agentverse_config(config) def sign(self, data: bytes) -> str: + """ + Sign the provided data. + + Args: + data (bytes): The data to be signed. + + Returns: + str: The signature of the data. + + """ return self._identity.sign(data) def sign_digest(self, digest: bytes) -> str: + """ + Sign the provided digest. + + Args: + digest (bytes): The digest to be signed. + + Returns: + str: The signature of the digest. + + """ return self._identity.sign_digest(digest) def sign_registration(self) -> str: + """ + Sign the registration data for Almanac contract. + + Returns: + str: The signature of the registration data. + + Raises: + AssertionError: If the Almanac contract address is None. + + """ assert self._almanac_contract.address is not None return self._identity.sign_registration( str(self._almanac_contract.address), @@ -222,15 +394,46 @@ def sign_registration(self) -> str: ) def update_endpoints(self, endpoints: List[Dict[str, Any]]): + """ + Update the list of endpoints. + + Args: + endpoints (List[Dict[str, Any]]): List of endpoint dictionaries. + + """ + self._endpoints = endpoints def update_loop(self, loop): + """ + Update the event loop. + + Args: + loop: The event loop. + + """ + self._loop = loop def update_queries(self, queries): + """ + Update the queries attribute. + + Args: + queries: The queries attribute. + + """ + self._queries = queries async def register(self): + """ + Register with the Almanac contract. + + This method checks for registration conditions and performs registration + if necessary. + + """ if self._endpoints is None: self._logger.warning( "I have no endpoint and cannot receive external messages" @@ -271,20 +474,42 @@ async def register(self): self._logger.info("Almanac registration is up to date!") async def _registration_loop(self): - await self.register() - # schedule the next registration + """ + Execute the registration loop. + + This method registers with the Almanac contract and schedules the next + registration. + + """ + + time_until_next_registration = REGISTRATION_UPDATE_INTERVAL_SECONDS + try: + await self.register() + except Exception as ex: + self._logger.exception(f"Failed to register on almanac contract: {ex}") + time_until_next_registration = REGISTRATION_RETRY_INTERVAL_SECONDS + # schedule the next registration update self._loop.create_task( - _delay(self._registration_loop(), self._schedule_registration()) + _delay(self._registration_loop(), time_until_next_registration) ) - def _schedule_registration(self): - return self._almanac_contract.get_expiry(self.address) - def on_interval( self, period: float, messages: Optional[Union[Type[Model], Set[Type[Model]]]] = None, ): + """ + Set up an interval event with a callback. + + Args: + period (float): The interval period. + messages (Optional[Union[Type[Model], Set[Type[Model]]]]): Optional message types. + + Returns: + Callable: The callback function for the interval event. + + """ + return self._protocol.on_interval(period, messages) def on_query( @@ -292,6 +517,18 @@ def on_query( model: Type[Model], replies: Optional[Union[Model, Set[Model]]] = None, ): + """ + Set up a query event with a callback. + + Args: + model (Type[Model]): The query model. + replies (Optional[Union[Model, Set[Model]]]): Optional reply models. + + Returns: + Callable: The callback function for the query event. + + """ + return self._protocol.on_query(model, replies) def on_message( @@ -300,10 +537,45 @@ def on_message( replies: Optional[Union[Type[Model], Set[Type[Model]]]] = None, allow_unverified: Optional[bool] = False, ): + """ + Set up a message event with a callback. + + Args: + model (Type[Model]): The message model. + replies (Optional[Union[Type[Model], Set[Type[Model]]]]): Optional reply models. + allow_unverified (Optional[bool]): Allow unverified messages. + + Returns: + Callable: The callback function for the message event. + + """ + return self._protocol.on_message(model, replies, allow_unverified) def on_event(self, event_type: str): + """ + Decorator to register an event handler for a specific event type. + + Args: + event_type (str): The type of event. + + Returns: + Callable: The decorator function for registering event handlers. + + """ + def decorator_on_event(func: EventCallback) -> EventCallback: + """ + Decorator function to register an event handler for a specific event type. + + Args: + func (EventCallback): The event handler function. + + Returns: + EventCallback: The decorated event handler function. + + """ + @functools.wraps(func) def handler(*args, **kwargs): return func(*args, **kwargs) @@ -319,12 +591,33 @@ def _add_event_handler( event_type: str, func: EventCallback, ) -> None: + """ + Add an event handler function to the specified event type. + + Args: + event_type (str): The type of event. + func (EventCallback): The event handler function. + + """ + if event_type == "startup": self._on_startup.append(func) elif event_type == "shutdown": self._on_shutdown.append(func) def include(self, protocol: Protocol, publish_manifest: Optional[bool] = False): + """ + Include a protocol into the agent's capabilities. + + Args: + protocol (Protocol): The protocol to include. + publish_manifest (Optional[bool]): Flag to publish the protocol's manifest. + + Raises: + RuntimeError: If a duplicate model, signed message handler, or message handler + is encountered. + + """ for func, period in protocol.intervals: self._interval_handlers.append((func, period)) @@ -358,6 +651,13 @@ def include(self, protocol: Protocol, publish_manifest: Optional[bool] = False): self.publish_manifest(protocol.manifest()) def publish_manifest(self, manifest: Dict[str, Any]): + """ + Publish a protocol manifest to the Almanac. + + Args: + manifest (Dict[str, Any]): The protocol manifest. + + """ try: resp = requests.post( f"{self._agentverse['http_prefix']}://{self._agentverse['base_url']}" @@ -377,18 +677,40 @@ def publish_manifest(self, manifest: Dict[str, Any]): async def handle_message( self, sender, schema_digest: str, message: JsonStr, session: uuid.UUID ): + """ + Handle an incoming message asynchronously. + + Args: + sender: The sender of the message. + schema_digest (str): The schema digest of the message. + message (JsonStr): The message content in JSON format. + session (uuid.UUID): The session UUID. + + """ await self._message_queue.put((schema_digest, sender, message, session)) async def _startup(self): + """ + Perform startup actions asynchronously. + + """ await self._registration_loop() for handler in self._on_startup: await handler(self._ctx) async def _shutdown(self): + """ + Perform shutdown actions asynchronously. + + """ for handler in self._on_shutdown: await handler(self._ctx) def setup(self): + """ + Set up the agent. + + """ # register the internal agent protocol self.include(self._protocol) self._loop.run_until_complete(self._startup()) @@ -399,6 +721,10 @@ def setup(self): self.start_background_tasks() def start_background_tasks(self): + """ + Start background tasks for the agent. + + """ # Start the interval tasks for func, period in self._interval_handlers: task = self._loop.create_task(_run_interval(func, self._ctx, period)) @@ -411,6 +737,10 @@ def start_background_tasks(self): task.add_done_callback(self._background_tasks.discard) def run(self): + """ + Run the agent. + + """ self.setup() try: if self._use_mailbox: @@ -422,6 +752,10 @@ def run(self): self._loop.run_until_complete(self._shutdown()) async def _process_message_queue(self): + """ + Process the message queue asynchronously. + + """ while True: # get an element from the queue schema_digest, sender, message, session = await self._message_queue.get() @@ -475,11 +809,41 @@ async def _process_message_queue(self): class Bureau: + """ + A class representing a Bureau of agents. + + This class manages a collection of agents and orchestrates their execution. + + Args: + port (Optional[int]): The port number for the server. + endpoint (Optional[Union[str, List[str], Dict[str, dict]]]): Configuration + for agent endpoints. + + Attributes: + _loop (asyncio.AbstractEventLoop): The event loop. + _agents (List[Agent]): A list of Agent instances within the bureau. + _endpoints (List[Dict[str, Any]]): A list of endpoint dictionaries for the agents. + _port (int): The port number for the server. + _queries (Dict[str, asyncio.Future]): A dictionary of query identifiers to asyncio futures. + _logger (Logger): The logger instance. + _server (ASGIServer): The ASGI server instance for handling requests. + _use_mailbox (bool): A flag indicating whether mailbox functionality is enabled. + + """ + def __init__( self, port: Optional[int] = None, endpoint: Optional[Union[str, List[str], Dict[str, dict]]] = None, ): + """ + Initialize a Bureau instance. + + Args: + port (Optional[int]): The port number for the server. + endpoint (Optional[Union[str, List[str], Dict[str, dict]]]): Configuration + for agent endpoints. + """ self._loop = asyncio.get_event_loop_policy().get_event_loop() self._agents: List[Agent] = [] self._endpoints = parse_endpoint_config(endpoint) @@ -490,6 +854,13 @@ def __init__( self._use_mailbox = False def add(self, agent: Agent): + """ + Add an agent to the bureau. + + Args: + agent (Agent): The agent instance to be added. + + """ agent.update_loop(self._loop) agent.update_queries(self._queries) if agent.agentverse["use_mailbox"]: @@ -499,6 +870,10 @@ def add(self, agent: Agent): self._agents.append(agent) def run(self): + """ + Run the agents managed by the bureau. + + """ tasks = [] for agent in self._agents: agent.setup() diff --git a/src/uagents/config.py b/src/uagents/config.py index b56e3414..cdad628b 100644 --- a/src/uagents/config.py +++ b/src/uagents/config.py @@ -22,8 +22,9 @@ class AgentNetwork(Enum): ) REGISTRATION_FEE = 500000000000000000 REGISTRATION_DENOM = "atestfet" -MIN_REGISTRATION_TIME = 3600 -BLOCK_INTERVAL = 5 +REGISTRATION_UPDATE_INTERVAL_SECONDS = 3600 +REGISTRATION_RETRY_INTERVAL_SECONDS = 60 +AVERAGE_BLOCK_INTERVAL = 5.7 AGENT_NETWORK = AgentNetwork.FETCHAI_TESTNET AGENTVERSE_URL = "https://agentverse.ai" @@ -89,4 +90,4 @@ def get_logger(logger_name): ) logger.addHandler(log_handler) logger.propagate = False - return logger + return logger \ No newline at end of file diff --git a/src/uagents/context.py b/src/uagents/context.py index b88f779b..40b09d33 100644 --- a/src/uagents/context.py +++ b/src/uagents/context.py @@ -1,3 +1,5 @@ +"""Agent Context and Message Handling""" + from __future__ import annotations import asyncio import logging @@ -28,6 +30,14 @@ @dataclass class MsgDigest: + """ + Represents a message digest containing a message and its schema digest. + + Attributes: + message (Any): The message content. + schema_digest (str): The schema digest of the message. + """ + message: Any schema_digest: str @@ -36,6 +46,38 @@ class MsgDigest: class Context: + """ + Represents the context in which messages are handled and processed. + + Attributes: + storage (KeyValueStore): The key-value store for storage operations. + wallet (LocalWallet): The local wallet instance for managing identities. + ledger (LedgerClient): The ledger client for interacting with distributed ledgers. + _name (Optional[str]): The optional name associated with the context. + _address (str): The address of the context. + _resolver (Resolver): The resolver for name-to-address resolution. + _identity (Identity): The identity associated with the context. + _queries (Dict[str, asyncio.Future]): Dictionary of query names and their asyncio Futures. + _session (Optional[uuid.UUID]): The optional session UUID. + _replies (Optional[Dict[str, Set[Type[Model]]]]): The optional dictionary of reply models. + _interval_messages (Optional[Set[str]]): The optional set of interval messages. + _message_received (Optional[MsgDigest]): The optional message digest received. + _protocols (Optional[Dict[str, Protocol]]): The optional dictionary of protocols. + _logger (Optional[logging.Logger]): The optional logger instance. + + Properties: + name (str): The name associated with the context, or a truncated address if name is None. + address (str): The address of the context. + logger (logging.Logger): The logger instance. + protocols (Optional[Dict[str, Protocol]]): The dictionary of protocols. + + Methods: + get_message_protocol(message_schema_digest): Get the protocol associated + with a message schema digest. + send(destination, message, timeout): Send a message to a destination. + + """ + def __init__( self, address: str, @@ -53,6 +95,25 @@ def __init__( protocols: Optional[Dict[str, Protocol]] = None, logger: Optional[logging.Logger] = None, ): + """ + Initialize the Context instance. + + Args: + address (str): The address of the context. + name (Optional[str]): The optional name associated with the context. + storage (KeyValueStore): The key-value store for storage operations. + resolve (Resolver): The resolver for name-to-address resolution. + identity (Identity): The identity associated with the context. + wallet (LocalWallet): The local wallet instance for managing identities. + ledger (LedgerClient): The ledger client for interacting with distributed ledgers. + queries (Dict[str, asyncio.Future]): Dictionary of query names and their Futures. + session (Optional[uuid.UUID]): The optional session UUID. + replies (Optional[Dict[str, Set[Type[Model]]]]): Optional dictionary of reply models. + interval_messages (Optional[Set[str]]): The optional set of interval messages. + message_received (Optional[MsgDigest]): The optional message digest received. + protocols (Optional[Dict[str, Protocol]]): The optional dictionary of protocols. + logger (Optional[logging.Logger]): The optional logger instance. + """ self.storage = storage self.wallet = wallet self.ledger = ledger @@ -70,27 +131,67 @@ def __init__( @property def name(self) -> str: + """ + Get the name associated with the context or a truncated address if name is None. + + Returns: + str: The name or truncated address. + """ if self._name is not None: return self._name return self._address[:10] @property def address(self) -> str: + """ + Get the address of the context. + + Returns: + str: The address of the context. + """ return self._address @property def logger(self) -> logging.Logger: + """ + Get the logger instance associated with the context. + + Returns: + logging.Logger: The logger instance. + """ return self._logger @property def protocols(self) -> Optional[Dict[str, Protocol]]: + """ + Get the dictionary of protocols associated with the context. + + Returns: + Optional[Dict[str, Protocol]]: The dictionary of protocols. + """ return self._protocols @property def session(self) -> uuid.UUID: + """ + Get the session UUID associated with the context. + + Returns: + uuid.UUID: The session UUID. + """ return self._session def get_message_protocol(self, message_schema_digest) -> Optional[str]: + """ + Get the protocol associated with a given message schema digest. + + Args: + message_schema_digest (str): The schema digest of the message. + + Returns: + Optional[str]: The protocol digest associated with the message schema digest, + or None if not found. + """ for protocol_digest, protocol in self._protocols.items(): for reply_models in protocol.replies.values(): if message_schema_digest in reply_models: @@ -103,6 +204,14 @@ async def send( message: Model, timeout: Optional[int] = DEFAULT_ENVELOPE_TIMEOUT_SECONDS, ): + """ + Send a message to the specified destination. + + Args: + destination (str): The destination address to send the message to. + message (Model): The message to be sent. + timeout (Optional[int]): The optional timeout for sending the message, in seconds. + """ schema_digest = Model.build_schema_digest(message) await self.send_raw( destination, @@ -120,7 +229,17 @@ async def send_raw( message_type: Optional[Type[Model]] = None, timeout: Optional[int] = DEFAULT_ENVELOPE_TIMEOUT_SECONDS, ): - # check if this message is a reply + """ + Send a raw message to the specified destination. + + Args: + destination (str): The destination address to send the message to. + json_message (JsonStr): The JSON-encoded message to be sent. + schema_digest (str): The schema digest of the message. + message_type (Optional[Type[Model]]): The optional type of the message being sent. + timeout (Optional[int]): The optional timeout for sending the message, in seconds. + """ + # Check if this message is a reply if ( self._message_received is not None and self._replies @@ -128,7 +247,7 @@ async def send_raw( ): received = self._message_received if received.schema_digest in self._replies: - # ensure the reply is valid + # Ensure the reply is valid if schema_digest not in self._replies[received.schema_digest]: self._logger.exception( f"Outgoing message {message_type or ''} " @@ -136,7 +255,7 @@ async def send_raw( ) return - # check if this message is a valid interval message + # Check if this message is a valid interval message if self._message_received is None and self._interval_messages: if schema_digest not in self._interval_messages: self._logger.exception( @@ -144,20 +263,20 @@ async def send_raw( ) return - # handle local dispatch of messages + # Handle local dispatch of messages if dispatcher.contains(destination): await dispatcher.dispatch( self.address, destination, schema_digest, json_message, self._session ) return - # handle queries waiting for a response + # Handle queries waiting for a response if destination in self._queries: self._queries[destination].set_result((json_message, schema_digest)) del self._queries[destination] return - # resolve the endpoint + # Resolve the endpoint destination_address, endpoint = await self._resolver.resolve(destination) if endpoint is None: self._logger.exception( @@ -165,10 +284,10 @@ async def send_raw( ) return - # calculate when envelope expires + # Calculate when the envelope expires expires = int(time()) + timeout - # handle external dispatch of messages + # Handle external dispatch of messages env = Envelope( version=1, sender=self.address, diff --git a/src/uagents/envelope.py b/src/uagents/envelope.py index 5349be10..8e87206f 100644 --- a/src/uagents/envelope.py +++ b/src/uagents/envelope.py @@ -1,3 +1,5 @@ +"""Agent Envelope.""" + import base64 import hashlib import struct @@ -10,6 +12,22 @@ class Envelope(BaseModel): + """ + Represents an envelope for message communication between agents. + + Attributes: + version (int): The envelope version. + sender (str): The sender's address. + target (str): The target's address. + session (UUID4): The session UUID. + schema_digest (str): The schema digest (alias for protocol). + protocol_digest (Optional[str]): The protocol digest (optional). + payload (Optional[str]): The payload data (optional). + expires (Optional[int]): The expiration timestamp (optional). + nonce (Optional[int]): The nonce value (optional). + signature (Optional[str]): The envelope signature (optional). + """ + version: int sender: str target: str @@ -25,24 +43,54 @@ class Config: allow_population_by_field_name = True def encode_payload(self, value: JsonStr): + """ + Encode the payload value and store it in the envelope. + + Args: + value (JsonStr): The payload value to be encoded. + """ self.payload = base64.b64encode(value.encode()).decode() def decode_payload(self) -> Optional[Any]: + """ + Decode and retrieve the payload value from the envelope. + + Returns: + Optional[Any]: The decoded payload value, or None if payload is not present. + """ if self.payload is None: return None return base64.b64decode(self.payload).decode() def sign(self, identity: Identity): + """ + Sign the envelope using the provided identity. + + Args: + identity (Identity): The identity used for signing. + """ self.signature = identity.sign_digest(self._digest()) def verify(self) -> bool: + """ + Verify the envelope's signature. + + Returns: + bool: True if the signature is valid, False otherwise. + """ if self.signature is None: return False return Identity.verify_digest(self.sender, self._digest(), self.signature) def _digest(self) -> bytes: + """ + Compute the digest of the envelope's content. + + Returns: + bytes: The computed digest. + """ hasher = hashlib.sha256() hasher.update(self.sender.encode()) hasher.update(self.target.encode()) diff --git a/src/uagents/network.py b/src/uagents/network.py index f9488a97..97470a64 100644 --- a/src/uagents/network.py +++ b/src/uagents/network.py @@ -1,3 +1,5 @@ +"""Network and Contracts.""" + import asyncio from datetime import datetime, timedelta from typing import Any, Optional, Dict, List @@ -22,7 +24,7 @@ CONTRACT_ALMANAC, CONTRACT_NAME_SERVICE, AGENT_NETWORK, - BLOCK_INTERVAL, + AVERAGE_BLOCK_INTERVAL, REGISTRATION_FEE, REGISTRATION_DENOM, get_logger, @@ -31,6 +33,7 @@ logger = get_logger("network") +# Setting up the Ledger and Faucet based on Agent Network if AGENT_NETWORK == AgentNetwork.FETCHAI_TESTNET: _ledger = LedgerClient(NetworkConfig.fetchai_stable_testnet()) _faucet_api = FaucetApi(NetworkConfig.fetchai_stable_testnet()) @@ -41,10 +44,22 @@ def get_ledger() -> LedgerClient: + """ + Get the Ledger client. + + Returns: + LedgerClient: The Ledger client instance. + """ return _ledger def get_faucet() -> FaucetApi: + """ + Get the Faucet API instance. + + Returns: + FaucetApi: The Faucet API instance. + """ return _faucet_api @@ -53,6 +68,19 @@ async def wait_for_tx_to_complete( timeout: Optional[timedelta] = None, poll_period: Optional[timedelta] = None, ) -> TxResponse: + """ + Wait for a transaction to complete on the Ledger. + + Args: + tx_hash (str): The hash of the transaction to monitor. + timeout (Optional[timedelta], optional): The maximum time to wait for + the transaction to complete. Defaults to None. + poll_period (Optional[timedelta], optional): The time interval to poll + the Ledger for the transaction status. Defaults to None. + + Returns: + TxResponse: The response object containing the transaction details. + """ if timeout is None: timeout = timedelta(seconds=DEFAULT_QUERY_TIMEOUT_SECS) if poll_period is None: @@ -72,7 +100,31 @@ async def wait_for_tx_to_complete( class AlmanacContract(LedgerContract): + """ + A class representing the Almanac contract for agent registration. + + This class provides methods to interact with the Almanac contract, including + checking if an agent is registered, retrieving the expiry height of an agent's + registration, and getting the endpoints associated with an agent's registration. + + Args: + ledger contract (LedgerContract): An instance of the LedgeContract class. + + Attributes: + ledger contract (LedgerContract): An instance of the LedgeContract class. + + """ + def is_registered(self, address: str) -> bool: + """ + Check if an agent is registered in the Almanac contract. + + Args: + address (str): The agent's address. + + Returns: + bool: True if the agent is registered, False otherwise. + """ query_msg = {"query_records": {"agent_address": address}} response = self.query(query_msg) @@ -80,7 +132,16 @@ def is_registered(self, address: str) -> bool: return False return True - def get_expiry(self, address: str): + def get_expiry(self, address: str) -> int: + """ + Get the expiry height of an agent's registration. + + Args: + address (str): The agent's address. + + Returns: + int: The expiry height of the agent's registration. + """ query_msg = {"query_records": {"agent_address": address}} response = self.query(query_msg) @@ -95,6 +156,15 @@ def get_expiry(self, address: str): return (expiry - height) * BLOCK_INTERVAL def get_endpoints(self, address: str): + """ + Get the endpoints associated with an agent's registration. + + Args: + address (str): The agent's address. + + Returns: + Any: The endpoints associated with the agent's registration. + """ query_msg = {"query_records": {"agent_address": address}} response = self.query(query_msg) @@ -103,6 +173,15 @@ def get_endpoints(self, address: str): return response.get("record")[0]["record"]["service"]["endpoints"] def get_protocols(self, address: str): + """ + Get the protocols associated with an agent's registration. + + Args: + address (str): The agent's address. + + Returns: + Any: The protocols associated with the agent's registration. + """ query_msg = {"query_records": {"agent_address": address}} response = self.query(query_msg) @@ -119,6 +198,17 @@ async def register( endpoints: List[Dict[str, Any]], signature: str, ): + """ + Register an agent with the Almanac contract. + + Args: + ledger (LedgerClient): The Ledger client. + wallet (LocalWallet): The agent's wallet. + agent_address (str): The agent's address. + protocols (List[str]): List of protocols. + endpoints (List[Dict[str, Any]]): List of endpoint dictionaries. + signature (str): The agent's signature. + """ transaction = Transaction() almanac_msg = { @@ -150,6 +240,15 @@ async def register( await wait_for_tx_to_complete(transaction.tx_hash) def get_sequence(self, address: str) -> int: + """ + Get the agent's sequence number. + + Args: + address (str): The agent's address. + + Returns: + int: The agent's sequence number. + """ query_msg = {"query_sequence": {"agent_address": address}} sequence = self.query(query_msg)["sequence"] @@ -160,15 +259,57 @@ def get_sequence(self, address: str) -> int: def get_almanac_contract() -> AlmanacContract: + """ + Get the AlmanacContract instance. + + Returns: + AlmanacContract: The AlmanacContract instance. + """ return _almanac_contract class NameServiceContract(LedgerContract): + """ + A class representing the NameService contract for managing domain names and ownership. + + This class provides methods to interact with the NameService contract, including + checking name availability, checking ownership, querying domain public status, + obtaining registration transaction details, and registering a name within a domain. + + Args: + ledger contract (LedgerContract): An instance of the LedgeContract class. + + Attributes: + ledger contract (LedgerContract): An instance of the LedgeContract class. + + """ + def is_name_available(self, name: str, domain: str): + """ + Check if a name is available within a domain. + + Args: + name (str): The name to check. + domain (str): The domain to check within. + + Returns: + bool: True if the name is available, False otherwise. + """ query_msg = {"domain_record": {"domain": f"{name}.{domain}"}} return self.query(query_msg)["is_available"] def is_owner(self, name: str, domain: str, wallet_address: str): + """ + Check if the provided wallet address is the owner of a name within a domain. + + Args: + name (str): The name to check ownership for. + domain (str): The domain to check within. + wallet_address (str): The wallet address to check ownership against. + + Returns: + bool: True if the wallet address is the owner, False otherwise. + """ query_msg = { "permissions": { "domain": f"{name}.{domain}", @@ -179,12 +320,34 @@ def is_owner(self, name: str, domain: str, wallet_address: str): return permission == "admin" def is_domain_public(self, domain: str): + """ + Check if a domain is public. + + Args: + domain (str): The domain to check. + + Returns: + bool: True if the domain is public, False otherwise. + """ res = self.query({"domain_record": {"domain": f".{domain}"}}) return res["is_public"] def get_registration_tx( self, name: str, wallet_address: str, agent_address: str, domain: str ): + """ + Get the registration transaction for registering a name within a domain. + + Args: + name (str): The name to be registered. + wallet_address (str): The wallet address initiating the registration. + agent_address (str): The address of the agent. + domain (str): The domain in which the name is registered. + + Returns: + Optional[Transaction]: The registration transaction, or None if the name is not + available or not owned by the wallet address. + """ if not self.is_name_available(name, domain) and not self.is_owner( name, domain, wallet_address ): @@ -214,6 +377,16 @@ async def register( name: str, domain: str, ): + """ + Register a name within a domain using the NameService contract. + + Args: + ledger (LedgerClient): The Ledger client. + wallet (LocalWallet): The wallet of the agent. + agent_address (str): The address of the agent. + name (str): The name to be registered. + domain (str): The domain in which the name is registered. + """ logger.info("Registering name...") if not get_almanac_contract().is_registered(agent_address): @@ -248,4 +421,10 @@ async def register( def get_name_service_contract() -> NameServiceContract: + """ + Get the NameServiceContract instance. + + Returns: + NameServiceContract: The NameServiceContract instance. + """ return _name_service_contract diff --git a/src/uagents/protocol.py b/src/uagents/protocol.py index 40e2a17d..f5b4f15b 100644 --- a/src/uagents/protocol.py +++ b/src/uagents/protocol.py @@ -1,3 +1,5 @@ +"""Exchange Protocol""" + import copy import functools import hashlib @@ -14,6 +16,13 @@ class Protocol: def __init__(self, name: Optional[str] = None, version: Optional[str] = None): + """ + Initialize a Protocol instance. + + Args: + name (Optional[str], optional): The name of the protocol. Defaults to None. + version (Optional[str], optional): The version of the protocol. Defaults to None. + """ self._interval_handlers: List[Tuple[IntervalCallback, float]] = [] self._interval_messages: Set[str] = set() self._signed_message_handlers: Dict[str, MessageCallback] = {} @@ -33,42 +42,105 @@ def __init__(self, name: Optional[str] = None, version: Optional[str] = None): @property def intervals(self): + """ + Property to access the interval handlers. + + Returns: + List[Tuple[IntervalCallback, float]]: List of interval handlers and their periods. + """ return self._interval_handlers @property def models(self): + """ + Property to access the registered models. + + Returns: + Dict[str, Type[Model]]: Dictionary of registered models with schema digests as keys. + """ return self._models @property def replies(self): + """ + Property to access the registered replies. + + Returns: + Dict[str, Dict[str, Type[Model]]]: Dictionary of registered replies with request + schema digests as keys. + """ return self._replies @property def interval_messages(self): + """ + Property to access the interval message digests. + + Returns: + Set[str]: Set of message digests associated with interval messages. + """ return self._interval_messages @property def signed_message_handlers(self): + """ + Property to access the signed message handlers. + + Returns: + Dict[str, MessageCallback]: Dictionary of signed message handlers with message schema + digests as keys. + """ return self._signed_message_handlers @property def unsigned_message_handlers(self): + """ + Property to access the unsigned message handlers. + + Returns: + Dict[str, MessageCallback]: Dictionary of unsigned message handlers with message schema + digests as keys. + """ return self._unsigned_message_handlers @property def name(self): + """ + Property to access the protocol name. + + Returns: + str: The protocol name. + """ return self._name @property def version(self): + """ + Property to access the protocol version. + + Returns: + str: The protocol version. + """ return self._version @property def canonical_name(self): + """ + Property to access the canonical name of the protocol. + + Returns: + str: The canonical name of the protocol. + """ return self._canonical_name @property def digest(self): + """ + Property to access the digest of the protocol's manifest. + + Returns: + str: The digest of the protocol's manifest. + """ return self.manifest()["metadata"]["digest"] def on_interval( @@ -76,6 +148,18 @@ def on_interval( period: float, messages: Optional[Union[Type[Model], Set[Type[Model]]]] = None, ): + """ + Decorator to register an interval handler for the protocol. + + Args: + period (float): The interval period in seconds. + messages (Optional[Union[Type[Model], Set[Type[Model]]]], optional): The associated + message types. Defaults to None. + + Returns: + Callable: The decorator to register the interval handler. + """ + def decorator_on_interval(func: IntervalCallback): @functools.wraps(func) def handler(*args, **kwargs): @@ -93,6 +177,14 @@ def _add_interval_handler( func: IntervalCallback, messages: Optional[Union[Type[Model], Set[Type[Model]]]], ): + """ + Add an interval handler to the protocol. + + Args: + period (float): The interval period in seconds. + func (IntervalCallback): The interval handler function. + messages (Optional[Union[Type[Model], Set[Type[Model]]]]): The associated message types. + """ # store the interval handler for later self._interval_handlers.append((func, period)) @@ -109,6 +201,17 @@ def on_query( model: Type[Model], replies: Optional[Union[Type[Model], Set[Type[Model]]]] = None, ): + """ + Decorator to register a query handler for the protocol. + + Args: + model (Type[Model]): The message model type. + replies (Optional[Union[Type[Model], Set[Type[Model]]]], optional): The associated + reply types. Defaults to None. + + Returns: + Callable: The decorator to register the query handler. + """ return self.on_message(model, replies, allow_unverified=True) def on_message( @@ -117,6 +220,20 @@ def on_message( replies: Optional[Union[Type[Model], Set[Type[Model]]]] = None, allow_unverified: Optional[bool] = False, ): + """ + Decorator to register a message handler for the protocol. + + Args: + model (Type[Model]): The message model type. + replies (Optional[Union[Type[Model], Set[Type[Model]]]], optional): The associated + reply types. Defaults to None. + allow_unverified (Optional[bool], optional): Whether to allow unverified messages. + Defaults to False. + + Returns: + Callable: The decorator to register the message handler. + """ + def decorator_on_message(func: MessageCallback): @functools.wraps(func) def handler(*args, **kwargs): @@ -135,6 +252,16 @@ def _add_message_handler( replies: Optional[Union[Type[Model], Set[Type[Model]]]], allow_unverified: Optional[bool] = False, ): + """ + Add a message handler to the protocol. + + Args: + model (Type[Model]): The message model type. + func (MessageCallback): The message handler function. + replies (Optional[Union[Type[Model], Set[Type[Model]]]]): The associated reply types. + allow_unverified (Optional[bool], optional): Whether to allow unverified messages. + Defaults to False. + """ model_digest = Model.build_schema_digest(model) # update the model database @@ -151,6 +278,12 @@ def _add_message_handler( } def manifest(self) -> Dict[str, Any]: + """ + Generate the protocol's manifest. + + Returns: + Dict[str, Any]: The protocol's manifest. + """ metadata = { "name": self._name, "version": self._version, @@ -195,7 +328,6 @@ def manifest(self) -> Dict[str, Any]: } ) - # print(schema_digest) encoded = json.dumps(manifest, indent=None, sort_keys=True).encode("utf8") metadata["digest"] = f"proto:{hashlib.sha256(encoded).digest().hex()}" @@ -206,6 +338,15 @@ def manifest(self) -> Dict[str, Any]: @staticmethod def compute_digest(manifest: Dict[str, Any]) -> str: + """ + Compute the digest of a given manifest. + + Args: + manifest (Dict[str, Any]): The manifest to compute the digest for. + + Returns: + str: The computed digest. + """ cleaned_manifest = copy.deepcopy(manifest) if "metadata" in cleaned_manifest: del cleaned_manifest["metadata"] diff --git a/src/uagents/query.py b/src/uagents/query.py index 52cd3cc3..bbacb6f6 100644 --- a/src/uagents/query.py +++ b/src/uagents/query.py @@ -1,3 +1,5 @@ +"""Query Envelopes.""" + import uuid from time import time from typing import Optional @@ -21,6 +23,19 @@ async def query( resolver: Optional[Resolver] = None, timeout: Optional[int] = 30, ) -> Optional[Envelope]: + """ + Query a remote agent with a message and retrieve the response envelope. + + Args: + destination (str): The destination address of the remote agent. + message (Model): The message to send. + resolver (Optional[Resolver], optional): The resolver to use for endpoint resolution. + Defaults to GlobalResolver. + timeout (Optional[int], optional): The timeout for the query in seconds. Defaults to 30. + + Returns: + Optional[Envelope]: The response envelope if successful, otherwise None. + """ if resolver is None: resolver = GlobalResolver() @@ -69,6 +84,17 @@ async def query( def enclose_response(message: Model, sender: str, session: str) -> str: + """ + Enclose a response message within an envelope. + + Args: + message (Model): The response message to enclose. + sender (str): The sender's address. + session (str): The session identifier. + + Returns: + str: The JSON representation of the response envelope. + """ schema_digest = Model.build_schema_digest(message) return enclose_response_raw(message.json(), schema_digest, sender, session) @@ -76,6 +102,18 @@ def enclose_response(message: Model, sender: str, session: str) -> str: def enclose_response_raw( json_message: JsonStr, schema_digest: str, sender: str, session: str ) -> str: + """ + Enclose a raw response message within an envelope. + + Args: + json_message (JsonStr): The JSON-formatted response message to enclose. + schema_digest (str): The schema digest of the message. + sender (str): The sender's address. + session (str): The session identifier. + + Returns: + str: The JSON representation of the response envelope. + """ response_env = Envelope( version=1, sender=sender, diff --git a/src/uagents/resolver.py b/src/uagents/resolver.py index 7190851c..ec3b2b0c 100644 --- a/src/uagents/resolver.py +++ b/src/uagents/resolver.py @@ -1,3 +1,5 @@ +"""Endpoint Resolver.""" + from abc import ABC, abstractmethod from typing import Dict, Optional import random @@ -6,6 +8,16 @@ def query_record(agent_address: str, service: str) -> dict: + """ + Query a record from the Almanac contract. + + Args: + agent_address (str): The address of the agent. + service (str): The type of service to query. + + Returns: + dict: The query result. + """ contract = get_almanac_contract() query_msg = { "query_record": {"agent_address": agent_address, "record_type": service} @@ -15,6 +27,15 @@ def query_record(agent_address: str, service: str) -> dict: def get_agent_address(name: str) -> str: + """ + Get the agent address associated with the provided name from the name service contract. + + Args: + name (str): The name to query. + + Returns: + str: The associated agent address. + """ query_msg = {"domain_record": {"domain": f"{name}"}} result = get_name_service_contract().query(query_msg) if result["record"] is not None: @@ -26,6 +47,15 @@ def get_agent_address(name: str) -> str: def is_agent_address(address): + """ + Check if the provided address is a valid agent address. + + Args: + address: The address to check. + + Returns: + bool: True if the address is a valid agent address, False otherwise. + """ if not isinstance(address, str): return False @@ -37,12 +67,31 @@ def is_agent_address(address): class Resolver(ABC): @abstractmethod + # pylint: disable=unnecessary-pass async def resolve(self, destination: str) -> Optional[str]: + """ + Resolve the destination to an endpoint. + + Args: + destination (str): The destination to resolve. + + Returns: + Optional[str]: The resolved endpoint or None. + """ pass class GlobalResolver(Resolver): async def resolve(self, destination: str) -> Optional[str]: + """ + Resolve the destination using a combination of Almanac and NameService resolvers. + + Args: + destination (str): The destination to resolve. + + Returns: + Optional[str]: The resolved endpoint or None. + """ almanac_resolver = AlmanacResolver() name_service_resolver = NameServiceResolver() address = ( @@ -58,6 +107,15 @@ async def resolve(self, destination: str) -> Optional[str]: class AlmanacResolver(Resolver): async def resolve(self, destination: str) -> Optional[str]: + """ + Resolve the destination using the Almanac contract. + + Args: + destination (str): The destination to resolve. + + Returns: + Optional[str]: The resolved endpoint or None. + """ result = query_record(destination, "service") if result is not None: record = result.get("record") or {} @@ -75,12 +133,36 @@ async def resolve(self, destination: str) -> Optional[str]: class NameServiceResolver(Resolver): async def resolve(self, destination: str) -> Optional[str]: + """ + Resolve the destination using the NameService contract. + + Args: + destination (str): The destination to resolve. + + Returns: + Optional[str]: The resolved endpoint or None. + """ return get_agent_address(destination) class RulesBasedResolver(Resolver): def __init__(self, rules: Dict[str, str]): + """ + Initialize the RulesBasedResolver with the provided rules. + + Args: + rules (Dict[str, str]): A dictionary of rules mapping destinations to endpoints. + """ self._rules = rules async def resolve(self, destination: str) -> Optional[str]: + """ + Resolve the destination using the provided rules. + + Args: + destination (str): The destination to resolve. + + Returns: + Optional[str]: The resolved endpoint or None. + """ return self._rules.get(destination) diff --git a/src/uagents/setup.py b/src/uagents/setup.py index 607060ee..50d189e8 100644 --- a/src/uagents/setup.py +++ b/src/uagents/setup.py @@ -1,3 +1,5 @@ +"""Agent's Setup.""" + import requests from cosmpy.crypto.address import Address @@ -9,6 +11,15 @@ def fund_agent_if_low(wallet_address: str): + """ + Checks the agent's wallet balance and adds funds if it's below the registration fee. + + Args: + wallet_address (str): The wallet address of the agent. + + Returns: + None + """ ledger = get_ledger() faucet = get_faucet() @@ -22,6 +33,16 @@ def fund_agent_if_low(wallet_address: str): def register_agent_with_mailbox(agent: Agent, email: str): + """ + Registers the agent on a mailbox server using the provided email. + + Args: + agent (Agent): The agent object to be registered. + email (str): The email address associated with the agent. + + Returns: + None + """ mailbox = agent.mailbox register_url = f"{mailbox['http_prefix']}://{mailbox['base_url']}/v1/auth/register" resp = requests.post( diff --git a/src/uagents/storage/__init__.py b/src/uagents/storage/__init__.py index be9ea0a9..9747e175 100644 --- a/src/uagents/storage/__init__.py +++ b/src/uagents/storage/__init__.py @@ -7,7 +7,35 @@ class KeyValueStore: + """ + A simple key-value store implementation for data storage. + + Attributes: + _data (dict): The internal data storage dictionary. + _name (str): The name associated with the store. + _path (str): The file path where the store data is stored. + + Methods: + __init__: Initialize the KeyValueStore instance. + get: Get the value associated with a key from the store. + has: Check if a key exists in the store. + set: Set a value associated with a key in the store. + remove: Remove a key and its associated value from the store. + clear: Clear all data from the store. + _load: Load data from the file into the store. + _save: Save the store data to the file. + + """ + def __init__(self, name: str, cwd: str = None): + """ + Initialize the KeyValueStore instance. + + Args: + name (str): The name associated with the store. + cwd (str, optional): The current working directory. Defaults to None. + + """ self._data = {} self._name = name or "my" @@ -46,6 +74,13 @@ def _save(self): def load_all_keys() -> dict: + """ + Load all private keys from the private keys file. + + Returns: + dict: A dictionary containing loaded private keys. + + """ private_keys_path = os.path.join(os.getcwd(), "private_keys.json") if os.path.exists(private_keys_path): with open(private_keys_path, encoding="utf-8") as load_file: @@ -54,6 +89,15 @@ def load_all_keys() -> dict: def save_private_keys(name: str, identity_key: str, wallet_key: str): + """ + Save private keys to the private keys file. + + Args: + name (str): The name associated with the private keys. + identity_key (str): The identity private key. + wallet_key (str): The wallet private key. + + """ private_keys = load_all_keys() private_keys[name] = {"identity_key": identity_key, "wallet_key": wallet_key} @@ -63,6 +107,16 @@ def save_private_keys(name: str, identity_key: str, wallet_key: str): def get_or_create_private_keys(name: str) -> Tuple[str, str]: + """ + Get or create private keys associated with a name. + + Args: + name (str): The name associated with the private keys. + + Returns: + Tuple[str, str]: A tuple containing the identity key and wallet key. + + """ keys = load_all_keys() if name in keys.keys(): private_keys = keys.get(name)