From bf625ddf8c26df71798b1423e75443ad64f34695 Mon Sep 17 00:00:00 2001 From: James Riehl Date: Mon, 25 Nov 2024 12:57:05 +0000 Subject: [PATCH 1/2] feat(core): add almanac api domain resolution --- python/src/uagents/resolver.py | 61 +++++++++++++++++++++++++++++----- 1 file changed, 52 insertions(+), 9 deletions(-) diff --git a/python/src/uagents/resolver.py b/python/src/uagents/resolver.py index 147391a8..3d1256a3 100644 --- a/python/src/uagents/resolver.py +++ b/python/src/uagents/resolver.py @@ -125,18 +125,18 @@ def query_record(agent_address: str, service: str, test: bool) -> dict: return result -def get_agent_address(name: str, test: bool) -> Optional[str]: +def _aname_contract_resolve(domain: str, test: bool) -> Optional[str]: """ - Get the agent address associated with the provided name from the name service contract. + Get the agent address associated with the provided domain from the name service contract. Args: - name (str): The name to query. + domain (str): The domain to query. test (bool): Whether to use the testnet or mainnet contract. Returns: Optional[str]: The associated agent address if found. """ - query_msg = {"domain_record": {"domain": f"{name}"}} + query_msg = {"domain_record": {"domain": f"{domain}"}} result = get_name_service_contract(test).query(query_msg) if result["record"] is not None: registered_records = result["record"]["records"][0]["agent_address"]["records"] @@ -183,7 +183,7 @@ def __init__( max_endpoints=self._max_endpoints, almanac_api_url=almanac_api_url ) self._name_service_resolver = NameServiceResolver( - max_endpoints=self._max_endpoints + max_endpoints=self._max_endpoints, almanac_api_url=almanac_api_url ) async def resolve(self, destination: str) -> Tuple[Optional[str], List[str]]: @@ -333,7 +333,9 @@ async def resolve(self, destination: str) -> Tuple[Optional[str], List[str]]: class NameServiceResolver(Resolver): - def __init__(self, max_endpoints: Optional[int] = None): + def __init__( + self, max_endpoints: Optional[int] = None, almanac_api_url: Optional[str] = None + ): """ Initialize the NameServiceResolver. @@ -341,10 +343,40 @@ def __init__(self, max_endpoints: Optional[int] = None): max_endpoints (Optional[int]): The maximum number of endpoints to return. """ self._max_endpoints = max_endpoints or DEFAULT_MAX_ENDPOINTS + self._almanac_api_url = almanac_api_url self._almanac_api_resolver = AlmanacApiResolver( - max_endpoints=self._max_endpoints + almanac_api_url=almanac_api_url, max_endpoints=self._max_endpoints ) + async def _api_resolve(self, domain: str) -> Optional[str]: + """ + Resolve the domain using the Alamanac API. + + Args: + domain (str): The domain to resolve. + + Returns: + Tuple[Optional[str], List[str]]: The address (if available) and resolved endpoints. + """ + try: + response = requests.get(f"{self._almanac_api_url}/domains/{domain}") + + if response.status_code != 200: + if response.status_code != 404: + LOGGER.debug( + f"Failed to resolve name {domain} from {self._almanac_api_url}: " + f"{response.status_code}: {response.text}" + ) + return None + + name_record = response.json() + agent_address = name_record.get("agent_address", None) + return agent_address + except Exception as ex: + LOGGER.error(f"Error when resolving {domain}: {ex}") + + return None + async def resolve(self, destination: str) -> Tuple[Optional[str], List[str]]: """ Resolve the destination using the NameService contract. @@ -355,11 +387,22 @@ async def resolve(self, destination: str) -> Tuple[Optional[str], List[str]]: Returns: Tuple[Optional[str], List[str]]: The address (if available) and resolved endpoints. """ - prefix, name, _ = parse_identifier(destination) + prefix, domain, _ = parse_identifier(destination) use_testnet = prefix != MAINNET_PREFIX - address = get_agent_address(name, use_testnet) + + if domain == "": + return None, [] + + # Prefer to resolve using the Almanac API + address = await self._api_resolve(domain) + + if address is None: + # If the API resolution fails, fallback to the ANAME contract + address = _aname_contract_resolve(domain, use_testnet) + if address is not None: return await self._almanac_api_resolver.resolve(address) + return None, [] From ce71680345e1c0fdbf32d19f883e98dda6001f6a Mon Sep 17 00:00:00 2001 From: James Riehl Date: Wed, 27 Nov 2024 13:50:05 +0000 Subject: [PATCH 2/2] feat: aname resolver updates --- python/src/uagents/config.py | 3 ++- python/src/uagents/network.py | 3 ++- python/src/uagents/resolver.py | 25 ++++++++++++++++++++++--- 3 files changed, 26 insertions(+), 5 deletions(-) diff --git a/python/src/uagents/config.py b/python/src/uagents/config.py index 0a8f9a7a..19fad167 100644 --- a/python/src/uagents/config.py +++ b/python/src/uagents/config.py @@ -8,6 +8,7 @@ TESTNET_PREFIX = "test-agent" MAINNET_PREFIX = "agent" AGENT_ADDRESS_LENGTH = 65 +TESTNET_CHAINID = "dorado-1" MAINNET_CONTRACT_ALMANAC = ( "fetch1mezzhfj7qgveewzwzdk6lz5sae4dunpmmsjr9u7z0tpmdsae8zmquq3y0y" @@ -19,7 +20,7 @@ "fetch1479lwv5vy8skute5cycuz727e55spkhxut0valrcm38x9caa2x8q99ef0q" ) TESTNET_CONTRACT_NAME_SERVICE = ( - "fetch1mxz8kn3l5ksaftx8a9pj9a6prpzk2uhxnqdkwuqvuh37tw80xu6qges77l" + "fetch1kewgfwxwtuxcnppr547wj6sd0e5fkckyp48dazsh89hll59epgpspmh0tn" ) REGISTRATION_FEE = 500000000000000000 REGISTRATION_DENOM = "atestfet" diff --git a/python/src/uagents/network.py b/python/src/uagents/network.py index 51dd25a6..433b2a52 100644 --- a/python/src/uagents/network.py +++ b/python/src/uagents/network.py @@ -29,6 +29,7 @@ MAINNET_CONTRACT_NAME_SERVICE, REGISTRATION_DENOM, REGISTRATION_FEE, + TESTNET_CHAINID, TESTNET_CONTRACT_ALMANAC, TESTNET_CONTRACT_NAME_SERVICE, ) @@ -724,7 +725,7 @@ async def register( wallet.address(), records, domain, - chain_id == "dorado-1", + chain_id == TESTNET_CHAINID, ) if transaction is None: diff --git a/python/src/uagents/resolver.py b/python/src/uagents/resolver.py index 3d1256a3..7d830baa 100644 --- a/python/src/uagents/resolver.py +++ b/python/src/uagents/resolver.py @@ -8,6 +8,7 @@ import requests from dateutil import parser +from pydantic import BaseModel from uagents.config import ( AGENT_ADDRESS_LENGTH, @@ -24,6 +25,16 @@ LOGGER = get_logger("resolver", logging.WARNING) +class AgentRecord(BaseModel): + address: str + weight: float + + +class DomainRecord(BaseModel): + name: str + agents: List[AgentRecord] + + def weighted_random_sample( items: List[Any], weights: Optional[List[float]] = None, k: int = 1, rng=random ) -> List[Any]: @@ -369,9 +380,17 @@ async def _api_resolve(self, domain: str) -> Optional[str]: ) return None - name_record = response.json() - agent_address = name_record.get("agent_address", None) - return agent_address + domain_record = DomainRecord.model_validate(response.json()) + agent_records = domain_record.agents + if len(agent_records) == 0: + return None + elif len(agent_records) == 1: + return agent_records[0].address + else: + addresses = [val.address for val in agent_records] + weights = [val.weight for val in agent_records] + return weighted_random_sample(addresses, weights=weights, k=1)[0] + except Exception as ex: LOGGER.error(f"Error when resolving {domain}: {ex}")