From 11238add001f504c73e2453c87e4c8de2bd6f0dc Mon Sep 17 00:00:00 2001 From: James Riehl <33920192+jrriehl@users.noreply.github.com> Date: Thu, 12 Dec 2024 12:06:14 +0000 Subject: [PATCH] feat(core): updates to mailbox and agentverse registration process (#588) --- python/docs/api/uagents/agent.md | 47 +-- python/docs/api/uagents/config.md | 24 +- python/docs/api/uagents/mailbox.md | 89 ++--- python/examples/11-mailbox-agents/alice.py | 18 +- python/examples/11-mailbox-agents/bob.py | 15 +- python/examples/12-remote-agents/agent1.py | 1 - .../17-stateful-communication/agent1.py | 1 - .../17-stateful-communication/agent2.py | 1 - .../17-stateful-communication/agent3.py | 1 - .../17-stateful-communication/agent4.py | 1 - python/src/uagents/agent.py | 116 +++--- python/src/uagents/asgi.py | 35 +- python/src/uagents/config.py | 73 +++- python/src/uagents/mailbox.py | 369 +++++++++++------- python/tests/test_config.py | 102 +++-- 15 files changed, 468 insertions(+), 425 deletions(-) diff --git a/python/docs/api/uagents/agent.md b/python/docs/api/uagents/agent.md index 9fe67330..7cccfa2a 100644 --- a/python/docs/api/uagents/agent.md +++ b/python/docs/api/uagents/agent.md @@ -130,7 +130,7 @@ An agent that interacts within a communication environment. - `_logger` - The logger instance for logging agent activities. - `_endpoints` _List[AgentEndpoint]_ - List of endpoints at which the agent is reachable. - `_use_mailbox` _bool_ - Indicates if the agent uses a mailbox for communication. -- `_agentverse` _dict_ - Agentverse configuration settings. +- `_agentverse` _AgentverseConfig_ - Agentverse configuration settings. - `_mailbox_client` _MailboxClient_ - The client for interacting with the agentverse mailbox. - `_ledger` - The client for interacting with the blockchain ledger. - `_almanac_contract` - The almanac contract for registering agent addresses to endpoints. @@ -168,8 +168,7 @@ An agent that interacts within a communication environment. - `identifier` _str_ - The Agent Identifier, including network prefix and address. - `wallet` _LocalWallet_ - The agent's wallet for transacting on the ledger. - `storage` _KeyValueStore_ - The key-value store for storage operations. -- `mailbox` _Dict[str, str]_ - The mailbox configuration for the agent. -- `agentverse` _Dict[str, str]_ - The agentverse configuration for the agent. +- `agentverse` _AgentverseConfig_ - The agentverse configuration for the agent. - `mailbox_client` _MailboxClient_ - The client for interacting with the agentverse mailbox. - `protocols` _Dict[str, Protocol]_ - Dictionary mapping all supported protocol digests to their corresponding protocols. @@ -185,7 +184,8 @@ def __init__(name: Optional[str] = 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, + mailbox: bool = False, + proxy: bool = False, resolve: Optional[Resolver] = None, registration_policy: Optional[AgentRegistrationPolicy] = None, enable_wallet_messaging: Union[bool, Dict[str, str]] = False, @@ -208,7 +208,8 @@ Initialize an Agent instance. - `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. +- `mailbox` _bool_ - True if the agent will receive messages via an Agentverse mailbox. +- `proxy` _bool_ - True if the agent will receive messages via an Agentverse proxy endpoint. - `resolve` _Optional[Resolver]_ - The resolver to use for agent communication. - `enable_wallet_messaging` _Optional[Union[bool, Dict[str, str]]]_ - Whether to enable wallet messaging. If '{"chain_id": CHAIN_ID}' is provided, this sets the chain ID for @@ -328,29 +329,13 @@ Get the key-value store used by the agent for data storage. - `KeyValueStore` - The key-value store instance. - - -#### mailbox - -```python -@property -def mailbox() -> Dict[str, str] -``` - -Get the mailbox configuration of the agent. -Agentverse overrides it but mailbox is kept for backwards compatibility. - -**Returns**: - - Dict[str, str]: The mailbox configuration. - #### agentverse ```python @property -def agentverse() -> Dict[str, str] +def agentverse() -> AgentverseConfig ``` Get the agentverse configuration of the agent. @@ -419,22 +404,6 @@ Get the metadata associated with the agent. Dict[str, Any]: The metadata associated with the agent. - - -#### mailbox - -```python -@mailbox.setter -def mailbox(config: Union[str, Dict[str, str]]) -``` - -Set the mailbox configuration for the agent. -Agentverse overrides it but mailbox is kept for backwards compatibility. - -**Arguments**: - -- `config` _Union[str, Dict[str, str]]_ - The new mailbox configuration. - #### agentverse @@ -863,7 +832,7 @@ This class manages a collection of agents and orchestrates their execution. response Futures. - `_logger` _Logger_ - The logger instance. - `_server` _ASGIServer_ - The ASGI server instance for handling requests. -- `_agentverse` _Dict[str, str]_ - The agentverse configuration for the bureau. +- `_agentverse` _AgentverseConfig_ - The agentverse configuration for the bureau. - `_use_mailbox` _bool_ - A flag indicating whether mailbox functionality is enabled for any of the agents. - `_registration_policy` _AgentRegistrationPolicy_ - The registration policy for the bureau. diff --git a/python/docs/api/uagents/config.md b/python/docs/api/uagents/config.md index 48bda955..882e5de2 100644 --- a/python/docs/api/uagents/config.md +++ b/python/docs/api/uagents/config.md @@ -8,15 +8,27 @@ ```python def parse_endpoint_config( - endpoint: Optional[Union[str, List[str], Dict[str, dict]]] -) -> List[AgentEndpoint] + endpoint: Optional[Union[str, List[str], Dict[str, dict]]], + agentverse: AgentverseConfig, + mailbox: bool = False, + proxy: bool = False, + logger: Optional[logging.Logger] = None) -> List[AgentEndpoint] ``` Parse the user-provided endpoint configuration. +**Arguments**: + +- `endpoint` _Optional[Union[str, List[str], Dict[str, dict]]]_ - The endpoint configuration. +- `agentverse` _AgentverseConfig_ - The agentverse configuration. +- `mailbox` _bool_ - Whether to use the mailbox endpoint. +- `proxy` _bool_ - Whether to use the proxy endpoint. +- `logger` _Optional[logging.Logger]_ - The logger to use. + + **Returns**: - Optional[List[Dict[str, Any]]]: The parsed endpoint configuration. +- `Optional[List[AgentEndpoint]` - The parsed endpoint configuration. @@ -24,13 +36,13 @@ Parse the user-provided endpoint configuration. ```python def parse_agentverse_config( - config: Optional[Union[str, Dict[str, str]]] = None -) -> Dict[str, Union[str, bool, None]] + config: Optional[Union[str, Dict[str, + str]]] = None) -> AgentverseConfig ``` Parse the user-provided agentverse configuration. **Returns**: - Dict[str, Union[str, bool, None]]: The parsed agentverse configuration. +- `AgentverseConfig` - The parsed agentverse configuration. diff --git a/python/docs/api/uagents/mailbox.md b/python/docs/api/uagents/mailbox.md index c74e71e4..7a3320f0 100644 --- a/python/docs/api/uagents/mailbox.md +++ b/python/docs/api/uagents/mailbox.md @@ -2,67 +2,70 @@ # src.uagents.mailbox - + -## MailboxClient Objects +#### is`_`mailbox`_`agent ```python -class MailboxClient() +def is_mailbox_agent(endpoints: list[AgentEndpoint], + agentverse: AgentverseConfig) -> bool ``` -Client for interacting with the Agentverse mailbox server. +Check if the agent is a mailbox agent. - +**Returns**: -#### base`_`url +- `bool` - True if the agent is a mailbox agent, False otherwise. + + + +#### is`_`proxy`_`agent ```python -@property -def base_url() +def is_proxy_agent(endpoints: list[AgentEndpoint], + agentverse: AgentverseConfig) -> bool ``` -Property to access the base url of the mailbox server. +Check if the agent is a proxy agent. + +**Returns**: -Returns: The base url of the mailbox server. +- `bool` - True if the agent is a proxy agent, False otherwise. - + -#### agent`_`mailbox`_`key +#### register`_`in`_`agentverse ```python -@property -def agent_mailbox_key() +async def register_in_agentverse( + request: AgentverseConnectRequest, identity: Identity, + endpoints: list[AgentEndpoint], + agentverse: AgentverseConfig) -> RegistrationResponse ``` -Property to access the agent_mailbox_key of the mailbox server. +Registers agent in Agentverse -Returns: The agent_mailbox_key of the mailbox server. +**Arguments**: - +- `request` _AgentverseConnectRequest_ - Request object +- `identity` _Identity_ - Agent identity object +- `endpoints` _list[AgentEndpoint]_ - Endpoints of the agent +- `agentverse` _AgentverseConfig_ - Agentverse configuration + -#### protocol +**Returns**: -```python -@property -def protocol() -``` - -Property to access the protocol of the mailbox server. - -Returns: The protocol of the mailbox server {ws, wss, http, https}. +- `RegistrationResponse` - Registration - + -#### http`_`prefix +## MailboxClient Objects ```python -@property -def http_prefix() +class MailboxClient() ``` -Property to access the http prefix of the mailbox server. - -Returns: The http prefix of the mailbox server {http, https}. +Client for interacting with the Agentverse mailbox server. @@ -74,23 +77,3 @@ async def run() Runs the mailbox client. - - -#### start`_`polling - -```python -async def start_polling() -``` - -Runs the mailbox client. Acquires an access token if needed and then starts a polling loop. - - - -#### process`_`deletion`_`queue - -```python -async def process_deletion_queue() -``` - -Processes the deletion queue. Deletes envelopes from the mailbox server. - diff --git a/python/examples/11-mailbox-agents/alice.py b/python/examples/11-mailbox-agents/alice.py index 60c2fc46..8c0107c4 100644 --- a/python/examples/11-mailbox-agents/alice.py +++ b/python/examples/11-mailbox-agents/alice.py @@ -5,25 +5,11 @@ class Message(Model): message: str -# First generate a secure seed phrase (e.g. https://pypi.org/project/mnemonic/) -SEED_PHRASE = "put_your_seed_phrase_here" - -# Copy the address shown below -print(f"Your agent's address is: {Agent(seed=SEED_PHRASE).address}") - -# Then go to https://agentverse.ai, register your agent in the Mailroom -# and copy the agent's mailbox key -AGENT_MAILBOX_KEY = "put_your_AGENT_MAILBOX_KEY_here" - # Now your agent is ready to join the agentverse! -agent = Agent( - name="alice", - seed=SEED_PHRASE, - mailbox=f"{AGENT_MAILBOX_KEY}@https://agentverse.ai", -) +agent = Agent(name="alice", port=8008, mailbox=True) -@agent.on_message(model=Message, replies={Message}) +@agent.on_message(model=Message, replies=Message) async def handle_message(ctx: Context, sender: str, msg: Message): ctx.logger.info(f"Received message from {sender}: {msg.message}") diff --git a/python/examples/11-mailbox-agents/bob.py b/python/examples/11-mailbox-agents/bob.py index 72a50930..d3c1c62b 100644 --- a/python/examples/11-mailbox-agents/bob.py +++ b/python/examples/11-mailbox-agents/bob.py @@ -8,22 +8,9 @@ class Message(Model): # Copy ALICE_ADDRESS generated in alice.py ALICE_ADDRESS = "paste_alice_address_here" -# Generate a second seed phrase (e.g. https://pypi.org/project/mnemonic/) -SEED_PHRASE = "put_your_seed_phrase_here" - -# Copy the address shown below -print(f"Your agent's address is: {Agent(seed=SEED_PHRASE).address}") - -# Then go to https://agentverse.ai, register your agent in the Mailroom -# and copy the agent's mailbox key -AGENT_MAILBOX_KEY = "put_your_AGENT_MAILBOX_KEY_here" # Now your agent is ready to join the agentverse! -agent = Agent( - name="bob", - seed=SEED_PHRASE, - mailbox=f"{AGENT_MAILBOX_KEY}@https://agentverse.ai", -) +agent = Agent(name="bob", port=8009, mailbox=True) @agent.on_interval(period=2.0) diff --git a/python/examples/12-remote-agents/agent1.py b/python/examples/12-remote-agents/agent1.py index 0fc3e4e0..8d27df38 100644 --- a/python/examples/12-remote-agents/agent1.py +++ b/python/examples/12-remote-agents/agent1.py @@ -36,7 +36,6 @@ async def act_on_message(ctx: Context, sender: str, msg: Message): ctx.logger.info(f"Received message from {sender[-8:]}: {msg.message}") -print(f"Agent address: {alice.address}") print(f"Agent public URL: {http_tunnel.public_url}/submit") if __name__ == "__main__": diff --git a/python/examples/17-stateful-communication/agent1.py b/python/examples/17-stateful-communication/agent1.py index 0f6c3dcb..f3091e05 100644 --- a/python/examples/17-stateful-communication/agent1.py +++ b/python/examples/17-stateful-communication/agent1.py @@ -121,5 +121,4 @@ async def conclude_chitchat( if __name__ == "__main__": - print(f"Agent address: {agent.address}") agent.run() diff --git a/python/examples/17-stateful-communication/agent2.py b/python/examples/17-stateful-communication/agent2.py index f4af3b19..ec07b9f1 100644 --- a/python/examples/17-stateful-communication/agent2.py +++ b/python/examples/17-stateful-communication/agent2.py @@ -120,5 +120,4 @@ async def start_cycle(ctx: Context): if __name__ == "__main__": - print(f"Agent address: {agent.address}") agent.run() diff --git a/python/examples/17-stateful-communication/agent3.py b/python/examples/17-stateful-communication/agent3.py index f90489ee..187ae3a6 100644 --- a/python/examples/17-stateful-communication/agent3.py +++ b/python/examples/17-stateful-communication/agent3.py @@ -54,5 +54,4 @@ async def continue_chitchat( if __name__ == "__main__": - print(f"Agent address: {agent.address}") agent.run() diff --git a/python/examples/17-stateful-communication/agent4.py b/python/examples/17-stateful-communication/agent4.py index e3c0d68a..d12d2cb6 100644 --- a/python/examples/17-stateful-communication/agent4.py +++ b/python/examples/17-stateful-communication/agent4.py @@ -49,5 +49,4 @@ async def start_cycle(ctx: Context): agent.include(chitchat_dialogue) if __name__ == "__main__": - print(f"Agent address: {agent.address}") agent.run() diff --git a/python/src/uagents/agent.py b/python/src/uagents/agent.py index b0eae87a..95711930 100644 --- a/python/src/uagents/agent.py +++ b/python/src/uagents/agent.py @@ -32,6 +32,7 @@ REGISTRATION_RETRY_INTERVAL_SECONDS, REGISTRATION_UPDATE_INTERVAL_SECONDS, TESTNET_PREFIX, + AgentverseConfig, parse_agentverse_config, parse_endpoint_config, ) @@ -39,7 +40,13 @@ from uagents.crypto import Identity, derive_key_from_seed, is_user_address from uagents.dispatch import Sink, dispatcher from uagents.envelope import EnvelopeHistory, EnvelopeHistoryEntry -from uagents.mailbox import MailboxClient +from uagents.mailbox import ( + AgentverseConnectRequest, + MailboxClient, + RegistrationResponse, + is_mailbox_agent, + register_in_agentverse, +) from uagents.models import ErrorMessage, Model from uagents.network import ( InsufficientFundsError, @@ -212,7 +219,7 @@ class Agent(Sink): _logger: The logger instance for logging agent activities. _endpoints (List[AgentEndpoint]): List of endpoints at which the agent is reachable. _use_mailbox (bool): Indicates if the agent uses a mailbox for communication. - _agentverse (dict): Agentverse configuration settings. + _agentverse (AgentverseConfig): Agentverse configuration settings. _mailbox_client (MailboxClient): The client for interacting with the agentverse mailbox. _ledger: The client for interacting with the blockchain ledger. _almanac_contract: The almanac contract for registering agent addresses to endpoints. @@ -250,8 +257,7 @@ class Agent(Sink): identifier (str): The Agent Identifier, including network prefix and address. wallet (LocalWallet): The agent's wallet for transacting on the ledger. storage (KeyValueStore): The key-value store for storage operations. - mailbox (Dict[str, str]): The mailbox configuration for the agent. - agentverse (Dict[str, str]): The agentverse configuration for the agent. + agentverse (AgentverseConfig): The agentverse configuration for the agent. mailbox_client (MailboxClient): The client for interacting with the agentverse mailbox. protocols (Dict[str, Protocol]): Dictionary mapping all supported protocol digests to their corresponding protocols. @@ -266,7 +272,8 @@ def __init__( 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, + mailbox: bool = False, + proxy: bool = False, resolve: Optional[Resolver] = None, registration_policy: Optional[AgentRegistrationPolicy] = None, enable_wallet_messaging: Union[bool, Dict[str, str]] = False, @@ -288,7 +295,8 @@ def __init__( 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. + mailbox (bool): True if the agent will receive messages via an Agentverse mailbox. + proxy (bool): True if the agent will receive messages via an Agentverse proxy endpoint. resolve (Optional[Resolver]): The resolver to use for agent communication. enable_wallet_messaging (Optional[Union[bool, Dict[str, str]]]): Whether to enable wallet messaging. If '{"chain_id": CHAIN_ID}' is provided, this sets the chain ID for @@ -312,34 +320,22 @@ def __init__( self._initialize_wallet_and_identity(seed, name, wallet_key_derivation_index) self._logger = get_logger(self.name, level=log_level) + self._agentverse = parse_agentverse_config(agentverse) + # configure endpoints and mailbox - self._endpoints = parse_endpoint_config(endpoint) - self._use_mailbox = False + self._endpoints = parse_endpoint_config( + endpoint, self._agentverse, mailbox, proxy, self._logger + ) - if mailbox: - # agentverse config overrides mailbox config - # but mailbox is kept for backwards compatibility - if agentverse: - self._logger.warning( - "Ignoring the provided 'mailbox' configuration since 'agentverse' overrides it" - ) - else: - agentverse = mailbox - self._agentverse = parse_agentverse_config(agentverse) - self._use_mailbox = self._agentverse["use_mailbox"] + self._use_mailbox = is_mailbox_agent(self._endpoints, self._agentverse) if self._use_mailbox: - self._mailbox_client = MailboxClient(self, self._logger) - # if mailbox is provided, override endpoints with mailbox endpoint - self._endpoints = [ - AgentEndpoint( - url=f"{self.mailbox['http_prefix']}://{self.mailbox['base_url']}/v1/submit", - weight=1, - ) - ] + self._mailbox_client = MailboxClient( + self._identity, self._agentverse, self._logger + ) else: self._mailbox_client = None - self._almanac_api_url = f"{self._agentverse['http_prefix']}://{self._agentverse['base_url']}/v1/almanac" + self._almanac_api_url = f"{self._agentverse.url}/v1/almanac" self._resolver = resolve or GlobalResolver( max_endpoints=max_resolver_endpoints, almanac_api_url=self._almanac_api_url, @@ -416,6 +412,12 @@ async def _handle_get_info(_ctx: Context): async def _handle_get_messages(_ctx: Context): return self._message_cache + @self.on_rest_post("/prove", AgentverseConnectRequest, RegistrationResponse) + async def _handle_prove(_ctx: Context, token: AgentverseConnectRequest): + return await register_in_agentverse( + token, self._identity, self._endpoints, self._agentverse + ) + self._enable_agent_inspector = enable_agent_inspector self._init_done = True @@ -596,18 +598,7 @@ def storage(self) -> KeyValueStore: return self._storage @property - def mailbox(self) -> Dict[str, str]: - """ - Get the mailbox configuration of the agent. - Agentverse overrides it but mailbox is kept for backwards compatibility. - - Returns: - Dict[str, str]: The mailbox configuration. - """ - return self._agentverse - - @property - def agentverse(self) -> Dict[str, str]: + def agentverse(self) -> AgentverseConfig: """ Get the agentverse configuration of the agent. @@ -662,17 +653,6 @@ def metadata(self) -> Dict[str, Any]: """ return self._metadata - @mailbox.setter - def mailbox(self, config: Union[str, Dict[str, str]]): - """ - Set the mailbox configuration for the agent. - Agentverse overrides it but mailbox is kept for backwards compatibility. - - 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]]): """ @@ -919,7 +899,7 @@ def _on_rest( return lambda func: func def decorator_on_rest(func: RestHandler): - @functools.wraps(RestGetHandler if method == "GET" else RestPostHandler) + @functools.wraps(RestGetHandler if method == "GET" else RestPostHandler) # type: ignore def handler(*args, **kwargs): return func(*args, **kwargs) @@ -1027,8 +1007,7 @@ def publish_manifest(self, manifest: Dict[str, Any]): """ try: resp = requests.post( - f"{self._agentverse['http_prefix']}://{self._agentverse['base_url']}" - + "/v1/almanac/manifests", + f"{self._agentverse.url}/v1/almanac/manifests", json=manifest, timeout=5, ) @@ -1084,6 +1063,7 @@ async def _startup(self): Perform startup actions. """ + self._logger.info(f"Starting agent with address: {self.address}") if self._registration_policy: if self._endpoints: self.start_registration_loop() @@ -1183,10 +1163,7 @@ async def start_server(self): """ if self._enable_agent_inspector: - agentverse_url = ( - f"{self._agentverse['http_prefix']}://{self._agentverse['base_url']}" - ) - inspector_url = f"{agentverse_url}/inspect/" + inspector_url = f"{self._agentverse.url}/inspect/" escaped_uri = requests.utils.quote(f"http://127.0.0.1:{self._port}") self._logger.info( f"Agent inspector available at {inspector_url}" @@ -1358,7 +1335,7 @@ class Bureau: response Futures. _logger (Logger): The logger instance. _server (ASGIServer): The ASGI server instance for handling requests. - _agentverse (Dict[str, str]): The agentverse configuration for the bureau. + _agentverse (AgentverseConfig): The agentverse configuration for the bureau. _use_mailbox (bool): A flag indicating whether mailbox functionality is enabled for any of the agents. _registration_policy (AgentRegistrationPolicy): The registration policy for the bureau. @@ -1397,7 +1374,6 @@ def __init__( """ self._loop = loop or asyncio.get_event_loop_policy().get_event_loop() self._agents: List[Agent] = [] - self._endpoints = parse_endpoint_config(endpoint) self._port = port or 8000 self._queries: Dict[str, asyncio.Future] = {} self._logger = get_logger("bureau", log_level) @@ -1408,8 +1384,15 @@ def __init__( logger=self._logger, ) self._agentverse = parse_agentverse_config(agentverse) - self._use_mailbox = self._agentverse["use_mailbox"] - almanac_api_url = f"{self._agentverse['http_prefix']}://{self._agentverse['base_url']}/v1/almanac" + self._endpoints = parse_endpoint_config( + endpoint, self._agentverse, False, False, self._logger + ) + self._use_mailbox = any( + [ + is_mailbox_agent(agent._endpoints, self._agentverse) + for agent in self._agents + ] + ) almanac_contract = get_almanac_contract(test) if wallet and seed: @@ -1439,7 +1422,7 @@ def __init__( almanac_contract, test, logger=self._logger, - almanac_api=almanac_api_url, + almanac_api=f"{self._agentverse.url}/v1/almanac", ) if agents is not None: @@ -1456,7 +1439,7 @@ def _update_agent(self, agent: Agent): """ agent.update_loop(self._loop) agent.update_queries(self._queries) - if agent.agentverse["use_mailbox"]: + if is_mailbox_agent(agent._endpoints, self._agentverse): self._use_mailbox = True else: if agent._endpoints: @@ -1533,7 +1516,10 @@ async def run_async(self): return for agent in self._agents: await agent.setup() - if agent.agentverse["use_mailbox"] and agent.mailbox_client is not None: + if ( + is_mailbox_agent(agent._endpoints, self._agentverse) + and agent.mailbox_client is not None + ): tasks.append(agent.mailbox_client.run()) self._loop.create_task(self._schedule_registration()) diff --git a/python/src/uagents/asgi.py b/python/src/uagents/asgi.py index d3d14814..899f8962 100644 --- a/python/src/uagents/asgi.py +++ b/python/src/uagents/asgi.py @@ -29,7 +29,7 @@ HOST = "0.0.0.0" -RESERVED_ENDPOINTS = ["/submit", "/messages", "/agent_info"] +RESERVED_ENDPOINTS = ["/submit", "/messages", "/agent_info", "/prove"] async def _read_asgi_body(receive): @@ -75,7 +75,7 @@ def __init__( Tuple[str, RestMethod, str], RestHandlerDetails ] = {} self._logger = logger or get_logger("server") - self._server = None + self._server: Optional[uvicorn.Server] = None @property def server(self): @@ -133,19 +133,27 @@ async def _asgi_send( body: Optional[Union[Dict[str, Any], ErrorWrapper]] = None, ): header = ( - [[k.encode(), v.encode()] for k, v in headers.items()] if headers else None + [[k.encode(), v.encode()] for k, v in headers.items()] + if headers is not None + else [[b"content-type", b"application/json"]] ) - if body is None: - body = {} await send( { "type": "http.response.start", "status": status_code, - "headers": header or [[b"content-type", b"application/json"]], + "headers": header, } ) - await send({"type": "http.response.body", "body": json.dumps(body).encode()}) + + if body is None: + encoded_body = ( + b"{}" if [[b"content-type", b"application/json"]] in header else b"" + ) + else: + encoded_body = json.dumps(body).encode() + + await send({"type": "http.response.body", "body": encoded_body}) async def handle_readiness_probe(self, headers: CaseInsensitiveDict, send): """ @@ -154,7 +162,7 @@ async def handle_readiness_probe(self, headers: CaseInsensitiveDict, send): if b"x-uagents-address" not in headers: await self._asgi_send(send, headers={"x-uagents-status": "indeterminate"}) else: - address = headers[b"x-uagents-address"].decode() + address = headers[b"x-uagents-address"].decode() # type: ignore if not dispatcher.contains(address): await self._asgi_send(send, headers={"x-uagents-status": "not-ready"}) else: @@ -224,7 +232,7 @@ async def _handle_rest( }, ) return - destination = headers[b"x-uagents-address"].decode() + destination = headers[b"x-uagents-address"].decode() # type: ignore rest_handler = handlers.get(destination) else: destination, rest_handler = handlers.popitem() @@ -294,6 +302,11 @@ async def __call__(self, scope, receive, send): # pylint: disable=too-many-bra request_method = scope["method"] request_path = scope["path"] + # Handle OPTIONS preflight request for CORS + if request_method == "OPTIONS": + await self._asgi_send(send, 204, headers={}) + return + # check if the request is for a REST endpoint handlers = self._get_rest_handler_details(request_method, request_path) if handlers: @@ -316,7 +329,7 @@ async def __call__(self, scope, receive, send): # pylint: disable=too-many-bra await self.handle_missing_content_type(headers, send) return - if b"application/json" not in headers[b"content-type"]: + if b"application/json" not in headers[b"content-type"]: # type: ignore await self._asgi_send(send, 400, body={"error": "invalid content-type"}) return @@ -337,7 +350,7 @@ async def __call__(self, scope, receive, send): # pylint: disable=too-many-bra ) return - expects_response = headers.get(b"x-uagents-connection") == b"sync" + expects_response = headers.get(b"x-uagents-connection") == b"sync" # type: ignore if expects_response: # Add a future that will be resolved once the query is answered diff --git a/python/src/uagents/config.py b/python/src/uagents/config.py index 0a8f9a7a..01985bd2 100644 --- a/python/src/uagents/config.py +++ b/python/src/uagents/config.py @@ -1,6 +1,10 @@ -from typing import Dict, List, Optional, Union +import logging +from typing import Dict, List, Literal, Optional, Union + +from pydantic import BaseModel from uagents.types import AgentEndpoint +from uagents.utils import get_logger AGENT_PREFIX = "agent" LEDGER_PREFIX = "fetch" @@ -43,15 +47,53 @@ DEFAULT_SEARCH_LIMIT = 100 +AgentType = Literal["hosted", "local", "mailbox", "proxy", "custom"] + + +class AgentverseConfig(BaseModel): + base_url: str + protocol: str + http_prefix: str + + @property + def url(self) -> str: + return f"{self.http_prefix}://{self.base_url}" + + def parse_endpoint_config( endpoint: Optional[Union[str, List[str], Dict[str, dict]]], + agentverse: AgentverseConfig, + mailbox: bool = False, + proxy: bool = False, + logger: Optional[logging.Logger] = None, ) -> List[AgentEndpoint]: """ Parse the user-provided endpoint configuration. + Args: + endpoint (Optional[Union[str, List[str], Dict[str, dict]]]): The endpoint configuration. + agentverse (AgentverseConfig): The agentverse configuration. + mailbox (bool): Whether to use the mailbox endpoint. + proxy (bool): Whether to use the proxy endpoint. + logger (Optional[logging.Logger]): The logger to use. + Returns: - Optional[List[Dict[str, Any]]]: The parsed endpoint configuration. + Optional[List[AgentEndpoint]: The parsed endpoint configuration. """ + + logger = logger or get_logger("config") + + if endpoint: + if mailbox: + logger.warning("Endpoint configuration overrides mailbox setting.") + if proxy: + logger.warning("Endpoint configuration overrides proxy setting.") + elif mailbox and proxy: + logger.warning( + "Mailbox and proxy settings are mutually exclusive. " + "Defaulting to mailbox." + ) + if isinstance(endpoint, dict): endpoints = [ AgentEndpoint.model_validate( @@ -65,6 +107,10 @@ def parse_endpoint_config( ] elif isinstance(endpoint, str): endpoints = [AgentEndpoint.model_validate({"url": endpoint, "weight": 1})] + elif mailbox: + endpoints = [AgentEndpoint(url=f"{agentverse.url}/v1/submit", weight=1)] + elif proxy: + endpoints = [AgentEndpoint(url=f"{agentverse.url}/v1/proxy/submit", weight=1)] else: endpoints = [] return endpoints @@ -72,36 +118,31 @@ def parse_endpoint_config( def parse_agentverse_config( config: Optional[Union[str, Dict[str, str]]] = None, -) -> Dict[str, Union[str, bool, None]]: +) -> AgentverseConfig: """ Parse the user-provided agentverse configuration. Returns: - Dict[str, Union[str, bool, None]]: The parsed agentverse configuration. + AgentverseConfig: The parsed agentverse configuration. """ - agent_mailbox_key = None base_url = AGENTVERSE_URL protocol = None protocol_override = None if isinstance(config, str): if config.count("@") == 1: - agent_mailbox_key, base_url = config.split("@") + _, base_url = config.split("@") elif "://" in config: base_url = config - else: - agent_mailbox_key = config elif isinstance(config, dict): - agent_mailbox_key = config.get("agent_mailbox_key") base_url = config.get("base_url") or base_url protocol_override = config.get("protocol") if "://" in base_url: protocol, base_url = base_url.split("://") protocol = protocol_override or protocol or "https" http_prefix = "https" if protocol in {"wss", "https"} else "http" - return { - "agent_mailbox_key": agent_mailbox_key, - "base_url": base_url, - "protocol": protocol, - "http_prefix": http_prefix, - "use_mailbox": agent_mailbox_key is not None, - } + + return AgentverseConfig( + base_url=base_url, + protocol=protocol, + http_prefix=http_prefix, + ) diff --git a/python/src/uagents/mailbox.py b/python/src/uagents/mailbox.py index 81ea6e8a..b41a04ee 100644 --- a/python/src/uagents/mailbox.py +++ b/python/src/uagents/mailbox.py @@ -1,101 +1,229 @@ import asyncio -import json import logging +from datetime import datetime from typing import Optional import aiohttp -import pydantic -import websockets.exceptions from aiohttp.client_exceptions import ClientConnectorError -from websockets import connect +from pydantic import UUID4, BaseModel, ValidationError -from uagents.config import MAILBOX_POLL_INTERVAL_SECONDS -from uagents.crypto import is_user_address +from uagents.config import MAILBOX_POLL_INTERVAL_SECONDS, AgentType, AgentverseConfig +from uagents.crypto import Identity, is_user_address from uagents.dispatch import dispatcher from uagents.envelope import Envelope +from uagents.models import Model +from uagents.types import AgentEndpoint from uagents.utils import get_logger +logger = get_logger("mailbox") -class MailboxClient: - """Client for interacting with the Agentverse mailbox server.""" - def __init__(self, agent, logger: Optional[logging.Logger] = None): - self._agent = agent - self._access_token: Optional[str] = None - self._envelopes_to_delete = asyncio.Queue() - self._poll_interval = MAILBOX_POLL_INTERVAL_SECONDS - self._logger = logger or get_logger("mailbox") +class AgentverseConnectRequest(Model): + user_token: str + agent_type: AgentType - @property - def base_url(self): - """ - Property to access the base url of the mailbox server. - Returns: The base url of the mailbox server. +class ChallengeRequest(BaseModel): + address: str - """ - return self._agent.mailbox["base_url"] - @property - def agent_mailbox_key(self): - """ - Property to access the agent_mailbox_key of the mailbox server. +class ChallengeResponse(BaseModel): + challenge: str - Returns: The agent_mailbox_key of the mailbox server. - """ - return self._agent.mailbox["agent_mailbox_key"] - @property - def protocol(self): - """ - Property to access the protocol of the mailbox server. +class ChallengeProof(BaseModel): + address: str + challenge: str + challenge_response: str - Returns: The protocol of the mailbox server {ws, wss, http, https}. - """ - return self._agent.mailbox["protocol"] - @property - def http_prefix(self): - """ - Property to access the http prefix of the mailbox server. +class ChallengeProofResponse(Model): + access_token: str + expiry: str + + +class RegistrationRequest(BaseModel): + address: str + challenge: str + challenge_response: str + agent_type: AgentType + endpoints: Optional[list[AgentEndpoint]] = None + + +class RegistrationResponse(Model): + success: bool + + +class StoredEnvelope(BaseModel): + uuid: UUID4 + envelope: Envelope + received_at: datetime + expires_at: datetime + + +def is_mailbox_agent( + endpoints: list[AgentEndpoint], agentverse: AgentverseConfig +) -> bool: + """ + Check if the agent is a mailbox agent. + + Returns: + bool: True if the agent is a mailbox agent, False otherwise. + """ + return any([f"{agentverse.url}/v1/submit" in ep.url for ep in endpoints]) + + +def is_proxy_agent( + endpoints: list[AgentEndpoint], agentverse: AgentverseConfig +) -> bool: + """ + Check if the agent is a proxy agent. + + Returns: + bool: True if the agent is a proxy agent, False otherwise. + """ + return any([f"{agentverse.url}/v1/proxy/submit" in ep.url for ep in endpoints]) + + +async def register_in_agentverse( + request: AgentverseConnectRequest, + identity: Identity, + endpoints: list[AgentEndpoint], + agentverse: AgentverseConfig, +) -> RegistrationResponse: + """ + Registers agent in Agentverse + + Args: + request (AgentverseConnectRequest): Request object + identity (Identity): Agent identity object + endpoints (list[AgentEndpoint]): Endpoints of the agent + agentverse (AgentverseConfig): Agentverse configuration + + Returns: + RegistrationResponse: Registration + """ + async with aiohttp.ClientSession() as session: + # get challenge + challenge_url = f"{agentverse.url}/v1/auth/challenge" + challenge_request = ChallengeRequest(address=identity.address) + logger.debug("Requesting mailbox access challenge") + async with session.post( + challenge_url, + data=challenge_request.model_dump_json(), + headers={ + "content-type": "application/json", + "Authorization": f"Bearer {request.user_token}", + }, + ) as resp: + resp.raise_for_status() + challenge = ChallengeResponse.model_validate_json(await resp.text()) + + # response to challenge with signature to get token + prove_url = f"{agentverse.url}/v1/agents" + async with session.post( + prove_url, + data=RegistrationRequest( + address=identity.address, + challenge=challenge.challenge, + challenge_response=identity.sign(challenge.challenge.encode()), + endpoints=endpoints, + agent_type=request.agent_type, + ).model_dump_json(), + headers={ + "content-type": "application/json", + "Authorization": f"Bearer {request.user_token}", + }, + ) as resp: + if resp.status == 409: + logger.exception("Agent is already registered in Agentverse.") + return RegistrationResponse(success=False) + resp.raise_for_status() + registration_response = RegistrationResponse.parse_raw(await resp.text()) + if registration_response.success: + logger.info( + f"Successfully registered as {request.agent_type} agent in Agentverse" + ) + + if request.agent_type == "mailbox" and not is_mailbox_agent(endpoints, agentverse): + logger.exception( + f"Agent endpoints {endpoints} do not match registered agent type: {request.agent_type}" + f"Please restart agent with endpoint='{agentverse.url}/v1/submit'" + ) + + return registration_response - Returns: The http prefix of the mailbox server {http, https}. - """ - return self._agent.mailbox["http_prefix"] + +class MailboxClient: + """Client for interacting with the Agentverse mailbox server.""" + + def __init__( + self, + identity: Identity, + agentverse: AgentverseConfig, + logger: Optional[logging.Logger] = None, + ): + self._identity = identity + self._agentverse = agentverse + self._access_token: Optional[str] = None + self._poll_interval = MAILBOX_POLL_INTERVAL_SECONDS + self._logger = logger or get_logger("mailbox") async def run(self): """ Runs the mailbox client. """ + self._logger.info(f"Starting mailbox client for {self._agentverse.url}") loop = asyncio.get_event_loop() - loop.create_task(self.start_polling()) - loop.create_task(self.process_deletion_queue()) + loop.create_task(self._check_mailbox_loop()) - async def start_polling(self): + async def _check_mailbox_loop(self): """ - Runs the mailbox client. Acquires an access token if needed and then starts a polling loop. + Retrieves envelopes from the mailbox server and processes them. """ - self._logger.info(f"Connecting to mailbox server at {self.base_url}") while True: try: - if self._access_token is None: - await self._get_access_token() - if self.protocol in {"ws", "wss"}: - await self._open_websocket_connection() - else: - await self._poll_server() - await asyncio.sleep(self._poll_interval) - except ClientConnectorError: - self._logger.exception("Failed to connect to mailbox server") + async with aiohttp.ClientSession() as session: + if self._access_token is None: + await self._get_access_token() + mailbox_url = f"{self._agentverse.url}/v1/mailbox" + async with session.get( + mailbox_url, + headers={ + "Authorization": f"token {self._access_token}", + }, + ) as resp: + success = resp.status == 200 + if success: + items = (await resp.json())["items"] + for item in items: + stored_env = StoredEnvelope.model_validate(item) + await self._handle_envelope(stored_env) + elif resp.status == 401: + self._access_token = None + self._logger.warning( + "Access token expired: a new one should be retrieved automatically" + ) + else: + self._logger.exception( + f"Failed to retrieve messages: {resp.status}:{(await resp.text())}" + ) + except ClientConnectorError as ex: + self._logger.warning(f"Failed to connect to mailbox server: {ex}") + + await asyncio.sleep(self._poll_interval) - async def _handle_envelope(self, payload: dict): + async def _handle_envelope(self, stored_env: StoredEnvelope): """ Handles an envelope received from the mailbox server. Dispatches the incoming messages and adds the envelope to the deletion queue. + + Args: + stored_env (StoredEnvelope): Envelope to handle """ try: - env = Envelope.model_validate(payload["envelope"]) - except pydantic.ValidationError: + env = Envelope.model_validate(stored_env.envelope) + except ValidationError: self._logger.warning("Received invalid envelope") return @@ -120,85 +248,34 @@ async def _handle_envelope(self, payload: dict): env.session, ) - # queue envelope for deletion from server - await self._envelopes_to_delete.put(payload) + # delete envelope from server + await self._delete_envelope(stored_env.uuid) - async def process_deletion_queue(self): - """ - Processes the deletion queue. Deletes envelopes from the mailbox server. + async def _delete_envelope(self, uuid: UUID4): """ - async with aiohttp.ClientSession() as session: - while True: - try: - env = await self._envelopes_to_delete.get() - env_url = ( - f"{self.http_prefix}://{self.base_url}/v1/mailbox/{env['uuid']}" - ) - self._logger.debug(f"Deleting message: {env}") - async with session.delete( - env_url, - headers={"Authorization": f"token {self._access_token}"}, - ) as resp: - if resp.status != 200: - self._logger.exception( - f"Failed to delete envelope from inbox: {(await resp.text())}" - ) - except ClientConnectorError as ex: - self._logger.warning(f"Failed to connect to mailbox server: {ex}") - except Exception as ex: - self._logger.exception( - f"Got exception while processing deletion queue: {ex}" - ) - - async def _poll_server(self): - """ - Polls the mailbox server for envelopes and handles them. - """ - async with aiohttp.ClientSession() as session: - # check the inbox for envelopes and handle them - mailbox_url = f"{self.http_prefix}://{self.base_url}/v1/mailbox" - async with session.get( - mailbox_url, - headers={"Authorization": f"token {self._access_token}"}, - ) as resp: - success = resp.status == 200 - if success: - items = (await resp.json())["items"] - for item in items: - await self._handle_envelope(item) - elif resp.status == 401: - self._access_token = None - self._logger.warning( - "Access token expired: a new one should be retrieved automatically" - ) - else: - self._logger.exception( - f"Failed to retrieve messages: {resp.status}:{(await resp.text())}" - ) + Deletes envelope from the mailbox server. - async def _open_websocket_connection(self): - """ - Opens a websocket connection to the mailbox server and handles incoming envelopes. + Args: + uuid (UUID4): UUID of the envelope to delete """ try: - async with connect( - f"{self.protocol}://{self.base_url}/v1/events?token={self._access_token}" - ) as websocket: - # wait for the event stream to come in - while True: - msg = await websocket.recv() - msg = json.loads(msg) - if msg["type"] == "envelope": - self._logger.debug(f"Got envelope: {msg['payload']}") - await self._handle_envelope(msg["payload"]) - - except websockets.exceptions.ConnectionClosedError: - self._logger.warning("Mailbox connection closed") - self._access_token = None - - except ConnectionRefusedError: - self._logger.warning("Mailbox connection refused") - self._access_token = None + async with aiohttp.ClientSession() as session: + env_url = f"{self._agentverse.url}/v1/mailbox/{str(uuid)}" + self._logger.debug(f"Deleting message: {str(uuid)}") + async with session.delete( + env_url, + headers={ + "Authorization": f"token {self._access_token}", + }, + ) as resp: + if resp.status != 200: + self._logger.exception( + f"Failed to delete envelope from inbox: {(await resp.text())}" + ) + except ClientConnectorError as ex: + self._logger.warning(f"Failed to connect to mailbox server: {ex}") + except Exception as ex: + self._logger.exception(f"Got exception while deleting message: {ex}") async def _get_access_token(self): """ @@ -206,10 +283,11 @@ async def _get_access_token(self): """ async with aiohttp.ClientSession() as session: # get challenge - challenge_url = f"{self.http_prefix}://{self.base_url}/v1/auth/challenge" + challenge_url = f"{self._agentverse.url}/v1/auth/challenge" + challenge_request = ChallengeRequest(address=self._identity.address) async with session.post( challenge_url, - data=json.dumps({"address": self._agent.address}), + data=challenge_request.model_dump_json(), headers={"content-type": "application/json"}, ) as resp: if resp and resp.status == 200: @@ -221,22 +299,23 @@ async def _get_access_token(self): return # response to challenge with signature to get token - prove_url = f"{self.http_prefix}://{self.base_url}/v1/auth/prove" + prove_url = f"{self._agentverse.url}/v1/auth/prove" + proof_request = ChallengeProof( + address=self._identity.address, + challenge=challenge, + challenge_response=self._identity.sign(challenge.encode()), + ) async with session.post( prove_url, - data=json.dumps( - { - "address": self._agent.address, - "agent_mailbox_key": self.agent_mailbox_key, - "challenge": challenge, - "challenge_response": self._agent.sign(challenge.encode()), - } - ), + data=proof_request.model_dump_json(), headers={"content-type": "application/json"}, ) as resp: if resp and resp.status == 200: + challenge_proof_response = ChallengeProofResponse.parse_raw( + await resp.text() + ) self._logger.info("Mailbox access token acquired") - self._access_token = (await resp.json())["access_token"] + self._access_token = challenge_proof_response.access_token else: self._logger.exception( f"Failed to prove authorization: {(await resp.text())}" diff --git a/python/tests/test_config.py b/python/tests/test_config.py index c3760788..a3b865ea 100644 --- a/python/tests/test_config.py +++ b/python/tests/test_config.py @@ -1,111 +1,103 @@ import unittest from uagents import Agent +from uagents.types import AgentEndpoint + +MAILBOX_ENDPOINT = "https://agentverse.ai/v1/submit" +PROXY_ENDPOINT = "https://agentverse.ai/v1/proxy/submit" + agents = [ Agent(), - Agent(mailbox="agent_mailbox_key@some_url"), - Agent(mailbox={"agent_mailbox_key": "agent_mailbox_key", "base_url": "some_url"}), - Agent(mailbox="agent_mailbox_key"), - Agent(agentverse="agent_mailbox_key@some_url"), - Agent(agentverse="agent_mailbox_key"), - Agent(agentverse="http://some_url"), - Agent(agentverse="wss://some_url"), - Agent(agentverse="ws://some_url"), - Agent(agentverse={"agent_mailbox_key": "agent_mailbox_key", "protocol": "wss"}), + Agent(mailbox="agent_mailbox_key"), # type: ignore (backwards compatibility) + Agent(agentverse="http://some_url", endpoint="http://some_url"), Agent(agentverse="https://staging.agentverse.ai"), Agent(agentverse={"base_url": "staging.agentverse.ai"}), + Agent(mailbox=True), + Agent(mailbox=True, endpoint="http://some_url"), + Agent(proxy=True), + Agent(proxy=True, endpoint="http://some_url"), + Agent(mailbox=True, proxy=True), +] + +expected_endpoints = [ + [], + [AgentEndpoint(url=MAILBOX_ENDPOINT, weight=1)], + [AgentEndpoint(url="http://some_url", weight=1)], + [], + [], + [AgentEndpoint(url=MAILBOX_ENDPOINT, weight=1)], + [AgentEndpoint(url="http://some_url", weight=1)], + [AgentEndpoint(url=PROXY_ENDPOINT, weight=1)], + [AgentEndpoint(url="http://some_url", weight=1)], + [AgentEndpoint(url=MAILBOX_ENDPOINT, weight=1)], ] expected_configs = [ { - "agent_mailbox_key": None, "base_url": "agentverse.ai", "protocol": "https", "http_prefix": "https", - "use_mailbox": False, }, { - "agent_mailbox_key": "agent_mailbox_key", - "base_url": "some_url", + "base_url": "agentverse.ai", "protocol": "https", "http_prefix": "https", - "use_mailbox": True, }, { - "agent_mailbox_key": "agent_mailbox_key", "base_url": "some_url", - "protocol": "https", - "http_prefix": "https", - "use_mailbox": True, + "protocol": "http", + "http_prefix": "http", }, { - "agent_mailbox_key": "agent_mailbox_key", - "base_url": "agentverse.ai", + "base_url": "staging.agentverse.ai", "protocol": "https", "http_prefix": "https", - "use_mailbox": True, }, { - "agent_mailbox_key": "agent_mailbox_key", - "base_url": "some_url", + "base_url": "staging.agentverse.ai", "protocol": "https", "http_prefix": "https", - "use_mailbox": True, }, { - "agent_mailbox_key": "agent_mailbox_key", "base_url": "agentverse.ai", "protocol": "https", "http_prefix": "https", - "use_mailbox": True, }, { - "agent_mailbox_key": None, - "base_url": "some_url", - "protocol": "http", - "http_prefix": "http", - "use_mailbox": False, - }, - { - "agent_mailbox_key": None, - "base_url": "some_url", - "protocol": "wss", + "base_url": "agentverse.ai", + "protocol": "https", "http_prefix": "https", - "use_mailbox": False, }, { - "agent_mailbox_key": None, - "base_url": "some_url", - "protocol": "ws", - "http_prefix": "http", - "use_mailbox": False, - }, - { - "agent_mailbox_key": "agent_mailbox_key", "base_url": "agentverse.ai", - "protocol": "wss", + "protocol": "https", "http_prefix": "https", - "use_mailbox": True, }, { - "agent_mailbox_key": None, - "base_url": "staging.agentverse.ai", + "base_url": "agentverse.ai", "protocol": "https", "http_prefix": "https", - "use_mailbox": False, }, { - "agent_mailbox_key": None, - "base_url": "staging.agentverse.ai", + "base_url": "agentverse.ai", "protocol": "https", "http_prefix": "https", - "use_mailbox": False, }, ] class TestConfig(unittest.TestCase): def test_parse_agentverse_config(self): - for agent, expected_config in zip(agents, expected_configs): # noqa - self.assertEqual(agent.agentverse, expected_config) + for agent, expected_config, endpoints, index in zip( # noqa + agents, + expected_configs, + expected_endpoints, + range(len(agents)), + ): + self.assertEqual( + agent.agentverse.model_dump(), + expected_config, + f"Failed on agent {index}", + ) + self.assertEqual(agent._endpoints, endpoints, f"Failed on agent {index}")