diff --git a/python/docs/api/uagents/agent.md b/python/docs/api/uagents/agent.md
index bb8f01d6..9d0efb55 100644
--- a/python/docs/api/uagents/agent.md
+++ b/python/docs/api/uagents/agent.md
@@ -159,6 +159,7 @@ An agent that interacts within a communication environment.
corresponding protocols.
- `_ctx` _Context_ - The context for agent interactions.
- `_test` _bool_ - True if the agent will register and transact on the testnet.
+- `_enable_agent_inspector` _bool_ - Enable the agent inspector REST endpoints.
Properties:
- `name` _str_ - The name of the agent.
@@ -732,6 +733,16 @@ def start_message_receivers()
Start message receiving tasks for the agent.
+
+
+#### start`_`server
+
+```python
+async def start_server()
+```
+
+Start the agent's server.
+
#### run`_`async
diff --git a/python/docs/api/uagents/context.md b/python/docs/api/uagents/context.md
index 21151e27..2a46794c 100644
--- a/python/docs/api/uagents/context.md
+++ b/python/docs/api/uagents/context.md
@@ -19,6 +19,7 @@ agent (AgentRepresentation): The agent representation associated with the contex
storage (KeyValueStore): The key-value store for storage operations.
ledger (LedgerClient): The client for interacting with the blockchain ledger.
logger (logging.Logger): The logger instance.
+session (uuid.UUID): The session UUID associated with the context.
**Methods**:
@@ -101,7 +102,7 @@ Get the logger instance associated with the context.
```python
@property
@abstractmethod
-def session() -> Union[uuid.UUID, None]
+def session() -> uuid.UUID
```
Get the session UUID associated with the context.
@@ -267,7 +268,7 @@ Represents the agent internal context for proactive behaviour.
```python
@property
-def session() -> Union[uuid.UUID, None]
+def session() -> uuid.UUID
```
Get the session UUID associated with the context.
@@ -339,7 +340,6 @@ Represents the reactive context in which messages are handled and processed.
- `_queries` _Dict[str, asyncio.Future]_ - Dictionary mapping query senders to their
response Futures.
-- `_session` _Optional[uuid.UUID]_ - The session UUID.
- `_replies` _Optional[Dict[str, Dict[str, Type[Model]]]]_ - Dictionary of allowed reply digests
for each type of incoming message.
- `_message_received` _Optional[MsgDigest]_ - The message digest received.
@@ -352,7 +352,6 @@ Represents the reactive context in which messages are handled and processed.
```python
def __init__(message_received: MsgDigest,
- session: Optional[uuid.UUID] = None,
queries: Optional[Dict[str, asyncio.Future]] = None,
replies: Optional[Dict[str, Dict[str, Type[Model]]]] = None,
protocol: Optional[Tuple[str, Protocol]] = None,
@@ -366,7 +365,6 @@ Initialize the ExternalContext instance and attributes needed from the InternalC
- `message_received` _MsgDigest_ - The optional message digest received.
- `queries` _Dict[str, asyncio.Future]_ - Dictionary mapping query senders to their
response Futures.
-- `session` _Optional[uuid.UUID]_ - The optional session UUID.
- `replies` _Optional[Dict[str, Dict[str, Type[Model]]]]_ - Dictionary of allowed replies
for each type of incoming message.
- `protocol` _Optional[Tuple[str, Protocol]]_ - The optional Tuple of protocols.
diff --git a/python/src/uagents/agent.py b/python/src/uagents/agent.py
index e2637a6f..792456ce 100644
--- a/python/src/uagents/agent.py
+++ b/python/src/uagents/agent.py
@@ -36,7 +36,7 @@
parse_agentverse_config,
parse_endpoint_config,
)
-from uagents.context import Context, ExternalContext, InternalContext
+from uagents.context import Context, ContextFactory, ExternalContext, InternalContext
from uagents.crypto import Identity, derive_key_from_seed, is_user_address
from uagents.dispatch import Sink, dispatcher
from uagents.envelope import EnvelopeHistory, EnvelopeHistoryEntry
@@ -71,24 +71,31 @@
from uagents.utils import get_logger
-async def _run_interval(func: IntervalCallback, ctx: Context, period: float):
+async def _run_interval(
+ func: IntervalCallback,
+ logger: logging.Logger,
+ context_factory: ContextFactory,
+ 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.
+ logger (logging.Logger): The logger instance for logging interval handler activities.
+ context_factory (ContextFactory): The factory function for creating the context.
period (float): The time period at which to run the callback function.
"""
while True:
try:
+ ctx = context_factory()
await func(ctx)
except OSError as ex:
- ctx.logger.exception(f"OS Error in interval handler: {ex}")
+ logger.exception(f"OS Error in interval handler: {ex}")
except RuntimeError as ex:
- ctx.logger.exception(f"Runtime Error in interval handler: {ex}")
+ logger.exception(f"Runtime Error in interval handler: {ex}")
except Exception as ex:
- ctx.logger.exception(f"Exception in interval handler: {ex}")
+ logger.exception(f"Exception in interval handler: {ex}")
await asyncio.sleep(period)
@@ -377,21 +384,6 @@ def __init__(
# keep track of supported protocols
self.protocols: Dict[str, Protocol] = {}
- self._ctx = InternalContext(
- agent=AgentRepresentation(
- address=self.address,
- name=self._name,
- signing_callback=self._identity.sign_digest,
- ),
- storage=self._storage,
- ledger=self._ledger,
- resolver=self._resolver,
- dispenser=self._dispenser,
- interval_messages=self._interval_messages,
- wallet_messaging_client=self._wallet_messaging_client,
- logger=self._logger,
- )
-
# register with the dispatcher
self._dispatcher.register(self.address, self)
@@ -426,6 +418,28 @@ async def _handle_get_messages(_ctx: Context):
self._init_done = True
+ def _build_context(self) -> InternalContext:
+ """
+ An internal method to build the context for the agent.
+
+ Returns:
+ InternalContext: The internal context for the agent.
+ """
+ return InternalContext(
+ agent=AgentRepresentation(
+ address=self.address,
+ name=self._name,
+ signing_callback=self._identity.sign_digest,
+ ),
+ storage=self._storage,
+ ledger=self._ledger,
+ resolver=self._resolver,
+ dispenser=self._dispenser,
+ interval_messages=self._interval_messages,
+ wallet_messaging_client=self._wallet_messaging_client,
+ logger=self._logger,
+ )
+
def _initialize_wallet_and_identity(self, seed, name, wallet_key_derivation_index):
"""
Initialize the wallet and identity for the agent.
@@ -997,7 +1011,10 @@ async def handle_rest(
if not handler:
return None
- args = (self._ctx, message) if message else (self._ctx,)
+ args = []
+ args.append(self._build_context())
+ if message:
+ args.append(message)
return await handler(*args) # type: ignore
@@ -1015,7 +1032,8 @@ async def _startup(self):
)
for handler in self._on_startup:
try:
- await handler(self._ctx)
+ ctx = self._build_context()
+ await handler(ctx)
except OSError as ex:
self._logger.exception(f"OS Error in startup handler: {ex}")
except RuntimeError as ex:
@@ -1030,7 +1048,8 @@ async def _shutdown(self):
"""
for handler in self._on_shutdown:
try:
- await handler(self._ctx)
+ ctx = self._build_context()
+ await handler(ctx)
except OSError as ex:
self._logger.exception(f"OS Error in shutdown handler: {ex}")
except RuntimeError as ex:
@@ -1061,7 +1080,9 @@ def start_interval_tasks(self):
"""
for func, period in self._interval_handlers:
- self._loop.create_task(_run_interval(func, self._ctx, period))
+ self._loop.create_task(
+ _run_interval(func, self._logger, self._build_context, period)
+ )
def start_message_receivers(self):
"""
@@ -1075,7 +1096,9 @@ def start_message_receivers(self):
if self._wallet_messaging_client is not None:
for task in [
self._wallet_messaging_client.poll_server(),
- self._wallet_messaging_client.process_message_queue(self._ctx),
+ self._wallet_messaging_client.process_message_queue(
+ self._build_context
+ ),
]:
self._loop.create_task(task)
@@ -1163,7 +1186,11 @@ async def _process_message_queue(self):
)
context = ExternalContext(
- agent=self._ctx.agent,
+ agent=AgentRepresentation(
+ address=self.address,
+ name=self._name,
+ signing_callback=self._identity.sign_digest,
+ ),
storage=self._storage,
ledger=self._ledger,
resolver=self._resolver,
@@ -1179,6 +1206,11 @@ async def _process_message_queue(self):
protocol=protocol_info,
)
+ # sanity check
+ assert (
+ context.session == session
+ ), "Context object should always have message session"
+
# parse the received message
try:
recovered = model_class.parse_raw(message)
diff --git a/python/src/uagents/context.py b/python/src/uagents/context.py
index a6139170..d2c8158d 100644
--- a/python/src/uagents/context.py
+++ b/python/src/uagents/context.py
@@ -10,6 +10,7 @@
from typing import (
TYPE_CHECKING,
Any,
+ Callable,
Dict,
List,
Optional,
@@ -59,6 +60,7 @@ class Context(ABC):
storage (KeyValueStore): The key-value store for storage operations.
ledger (LedgerClient): The client for interacting with the blockchain ledger.
logger (logging.Logger): The logger instance.
+ session (uuid.UUID): The session UUID associated with the context.
Methods:
get_agents_by_protocol(protocol_digest, limit, logger): Retrieve a list of agent addresses
@@ -116,7 +118,7 @@ def logger(self) -> logging.Logger:
@property
@abstractmethod
- def session(self) -> Union[uuid.UUID, None]:
+ def session(self) -> uuid.UUID:
"""
Get the session UUID associated with the context.
@@ -256,6 +258,7 @@ def __init__(
ledger: LedgerClient,
resolver: Resolver,
dispenser: Dispenser,
+ session: Optional[uuid.UUID] = None,
interval_messages: Optional[Set[str]] = None,
wallet_messaging_client: Optional[Any] = None,
logger: Optional[logging.Logger] = None,
@@ -266,7 +269,7 @@ def __init__(
self._resolver = resolver
self._dispenser = dispenser
self._logger = logger
- self._session: Optional[uuid.UUID] = None
+ self._session = session or uuid.uuid4()
self._interval_messages = interval_messages
self._wallet_messaging_client = wallet_messaging_client
self._outbound_messages: Dict[str, Tuple[JsonStr, str]] = {}
@@ -288,7 +291,7 @@ def logger(self) -> Union[logging.Logger, None]:
return self._logger
@property
- def session(self) -> Union[uuid.UUID, None]:
+ def session(self) -> uuid.UUID:
"""
Get the session UUID associated with the context.
@@ -408,7 +411,6 @@ async def send(
we don't have access properties that are only necessary in re-active
contexts, like 'replies', 'message_received', or 'protocol'.
"""
- self._session = None
schema_digest = Model.build_schema_digest(message)
message_body = message.model_dump_json()
@@ -440,8 +442,6 @@ async def send_raw(
protocol_digest: Optional[str] = None,
queries: Optional[Dict[str, asyncio.Future]] = None,
) -> MsgStatus:
- self._session = self._session or uuid.uuid4()
-
# Extract address from destination agent identifier if present
_, parsed_name, parsed_address = parse_identifier(destination)
@@ -564,7 +564,6 @@ class ExternalContext(InternalContext):
Attributes:
_queries (Dict[str, asyncio.Future]): Dictionary mapping query senders to their
response Futures.
- _session (Optional[uuid.UUID]): The session UUID.
_replies (Optional[Dict[str, Dict[str, Type[Model]]]]): Dictionary of allowed reply digests
for each type of incoming message.
_message_received (Optional[MsgDigest]): The message digest received.
@@ -575,7 +574,6 @@ class ExternalContext(InternalContext):
def __init__(
self,
message_received: MsgDigest,
- session: Optional[uuid.UUID] = None,
queries: Optional[Dict[str, asyncio.Future]] = None,
replies: Optional[Dict[str, Dict[str, Type[Model]]]] = None,
protocol: Optional[Tuple[str, Protocol]] = None,
@@ -588,13 +586,11 @@ def __init__(
message_received (MsgDigest): The optional message digest received.
queries (Dict[str, asyncio.Future]): Dictionary mapping query senders to their
response Futures.
- session (Optional[uuid.UUID]): The optional session UUID.
replies (Optional[Dict[str, Dict[str, Type[Model]]]]): Dictionary of allowed replies
for each type of incoming message.
protocol (Optional[Tuple[str, Protocol]]): The optional Tuple of protocols.
"""
super().__init__(**kwargs)
- self._session = session or None
self._queries = queries or {}
self._replies = replies
self._message_received = message_received
@@ -674,3 +670,6 @@ async def send(
protocol_digest=self._protocol[0],
queries=self._queries,
)
+
+
+ContextFactory = Callable[[], Context]
diff --git a/python/src/uagents/registration.py b/python/src/uagents/registration.py
index 0a90c566..c44d94ea 100644
--- a/python/src/uagents/registration.py
+++ b/python/src/uagents/registration.py
@@ -20,7 +20,7 @@
)
from uagents.crypto import Identity
from uagents.network import AlmanacContract, InsufficientFundsError, add_testnet_funds
-from uagents.types import AgentEndpoint
+from uagents.types import AgentEndpoint, AgentGeoLocation
class AgentRegistrationPolicy(ABC):
@@ -32,15 +32,6 @@ async def register(
pass
-class AgentGeoLocation(BaseModel):
- # Latitude and longitude of the agent
- latitude: float
- longitude: float
-
- # Radius around the agent location, expressed in meters
- radius: float
-
-
class AgentRegistrationAttestation(BaseModel):
agent_address: str
protocols: List[str]
diff --git a/python/src/uagents/types.py b/python/src/uagents/types.py
index 1379dad9..31216894 100644
--- a/python/src/uagents/types.py
+++ b/python/src/uagents/types.py
@@ -42,6 +42,15 @@ class AgentEndpoint(BaseModel):
weight: int
+class AgentGeoLocation(BaseModel):
+ # Latitude and longitude of the agent
+ latitude: float
+ longitude: float
+
+ # Radius around the agent location, expressed in meters
+ radius: float
+
+
class AgentInfo(BaseModel):
agent_address: str
endpoints: List[AgentEndpoint]
diff --git a/python/src/uagents/wallet_messaging.py b/python/src/uagents/wallet_messaging.py
index 2c86355a..cb66b560 100644
--- a/python/src/uagents/wallet_messaging.py
+++ b/python/src/uagents/wallet_messaging.py
@@ -11,7 +11,7 @@
from requests import HTTPError, JSONDecodeError
from uagents.config import WALLET_MESSAGING_POLL_INTERVAL_SECONDS
-from uagents.context import Context
+from uagents.context import ContextFactory
from uagents.crypto import Identity
from uagents.types import WalletMessageCallback
from uagents.utils import get_logger
@@ -79,8 +79,9 @@ async def poll_server(self):
)
await asyncio.sleep(self._poll_interval)
- async def process_message_queue(self, ctx: Context):
+ async def process_message_queue(self, context_factory: ContextFactory): # noqa: F821
while True:
msg: WalletMessage = await self._message_queue.get()
for handler in self._message_handlers:
+ ctx = context_factory()
await handler(ctx, msg)
diff --git a/python/tests/test_agent.py b/python/tests/test_agent.py
index e8dcdde3..4d63bb2b 100644
--- a/python/tests/test_agent.py
+++ b/python/tests/test_agent.py
@@ -82,7 +82,7 @@ def _(ctx: Context):
startup_handlers = self.agent._on_startup
self.assertEqual(len(startup_handlers), 1)
self.assertTrue(isinstance(startup_handlers[0], Callable))
- self.assertIsNone(self.agent._ctx.storage.get("startup"))
+ self.assertIsNone(self.agent._storage.get("startup"))
def test_agent_on_shutdown_event(self):
@self.agent.on_event("shutdown")
@@ -92,7 +92,7 @@ def _(ctx: Context):
shutdown_handlers = self.agent._on_shutdown
self.assertEqual(len(shutdown_handlers), 1)
self.assertTrue(isinstance(shutdown_handlers[0], Callable))
- self.assertIsNone(self.agent._ctx.storage.get("shutdown"))
+ self.assertIsNone(self.agent._storage.get("shutdown"))
def test_agent_on_rest_get(self):
@self.agent.on_rest_get("/get", Response)
diff --git a/python/tests/test_context.py b/python/tests/test_context.py
index c15db9f9..34dae936 100644
--- a/python/tests/test_context.py
+++ b/python/tests/test_context.py
@@ -52,10 +52,8 @@ def setUp(self):
self.alice = Agent(name="alice", seed="alice recovery phrase", resolve=resolver)
self.bob = Agent(name="bob", seed="bob recovery phrase")
- self.agent = self.alice
- self.context = self.agent._ctx
self.loop = asyncio.get_event_loop()
- self.loop.create_task(self.context._dispenser.run())
+ self.loop.create_task(self.alice._dispenser.run())
def get_external_context(
self,
@@ -65,13 +63,13 @@ def get_external_context(
queries: Optional[Dict[str, asyncio.Future]] = None,
):
return ExternalContext(
- agent=self.context.agent,
- storage=self.agent._storage,
- ledger=self.agent._ledger,
- resolver=self.agent._resolver,
- dispenser=self.agent._dispenser,
- wallet_messaging_client=self.agent._wallet_messaging_client,
- logger=self.agent._logger,
+ agent=self.alice,
+ storage=self.alice._storage,
+ ledger=self.alice._ledger,
+ resolver=self.alice._resolver,
+ dispenser=self.alice._dispenser,
+ wallet_messaging_client=self.alice._wallet_messaging_client,
+ logger=self.alice._logger,
queries=queries,
session=None,
replies=replies,
@@ -79,13 +77,14 @@ def get_external_context(
)
async def test_send_local_dispatch(self):
- result = await self.context.send(self.bob.address, msg)
+ context = self.alice._build_context()
+ result = await context.send(self.bob.address, msg)
exp_msg_status = MsgStatus(
status=DeliveryStatus.DELIVERED,
detail="Message dispatched locally",
destination=self.bob.address,
endpoint="",
- session=self.context.session,
+ session=context.session,
)
self.assertEqual(result, exp_msg_status)
@@ -121,28 +120,29 @@ async def test_send_local_dispatch_invalid_reply(self):
self.assertEqual(result, exp_msg_status)
async def test_send_local_dispatch_valid_interval_msg(self):
- self.context._interval_messages = {msg_digest}
- result = await self.context.send(self.bob.address, msg)
+ context = self.alice._build_context()
+ context._interval_messages = {msg_digest}
+ result = await context.send(self.bob.address, msg)
exp_msg_status = MsgStatus(
status=DeliveryStatus.DELIVERED,
detail="Message dispatched locally",
destination=self.bob.address,
endpoint="",
- session=self.context.session,
+ session=context.session,
)
self.assertEqual(result, exp_msg_status)
- self.context._interval_messages = set()
async def test_send_local_dispatch_invalid_interval_msg(self):
- self.context._interval_messages = {msg_digest}
- result = await self.context.send(self.bob.address, incoming)
+ context = self.alice._build_context()
+ context._interval_messages = {msg_digest}
+ result = await context.send(self.bob.address, incoming)
exp_msg_status = MsgStatus(
status=DeliveryStatus.FAILED,
detail="Invalid interval message",
destination=self.bob.address,
endpoint="",
- session=self.context.session,
+ session=context.session,
)
self.assertEqual(result, exp_msg_status)
@@ -170,13 +170,14 @@ async def test_send_resolve_sync_query(self):
async def test_send_external_dispatch_resolve_failure(self):
destination = Identity.generate().address
- result = await self.context.send(destination, msg)
+ context = self.alice._build_context()
+ result = await context.send(destination, msg)
exp_msg_status = MsgStatus(
status=DeliveryStatus.FAILED,
detail="Unable to resolve destination endpoint",
destination=destination,
endpoint="",
- session=self.context.session,
+ session=context.session,
)
self.assertEqual(result, exp_msg_status)
@@ -186,8 +187,10 @@ async def test_send_external_dispatch_success(self, mocked_responses):
# Mock the HTTP POST request with a status code and response content
mocked_responses.post(endpoints[0], status=200)
+ context = self.alice._build_context()
+
# Perform the actual operation
- result = await self.context.send(self.clyde.address, msg)
+ result = await context.send(self.clyde.address, msg)
# Define the expected message status
exp_msg_status = MsgStatus(
@@ -195,7 +198,7 @@ async def test_send_external_dispatch_success(self, mocked_responses):
detail="Message successfully delivered via HTTP",
destination=self.clyde.address,
endpoint=endpoints[0],
- session=self.context.session,
+ session=context.session,
)
# Assertions
@@ -206,8 +209,10 @@ async def test_send_external_dispatch_failure(self, mocked_responses):
# Mock the HTTP POST request with a status code and response content
mocked_responses.post(endpoints[0], status=404)
+ context = self.alice._build_context()
+
# Perform the actual operation
- result = await self.context.send(self.clyde.address, msg)
+ result = await context.send(self.clyde.address, msg)
# Define the expected message status
exp_msg_status = MsgStatus(
@@ -215,7 +220,7 @@ async def test_send_external_dispatch_failure(self, mocked_responses):
detail="Message delivery failed",
destination=self.clyde.address,
endpoint="",
- session=self.context.session,
+ session=context.session,
)
# Assertions
@@ -231,8 +236,10 @@ async def test_send_external_dispatch_multiple_endpoints_first_success(
mocked_responses.post(endpoints[0], status=200)
mocked_responses.post(endpoints[1], status=404)
+ context = self.alice._build_context()
+
# Perform the actual operation
- result = await self.context.send(self.clyde.address, msg)
+ result = await context.send(self.clyde.address, msg)
# Define the expected message status
exp_msg_status = MsgStatus(
@@ -240,7 +247,7 @@ async def test_send_external_dispatch_multiple_endpoints_first_success(
detail="Message successfully delivered via HTTP",
destination=self.clyde.address,
endpoint=endpoints[0],
- session=self.context.session,
+ session=context.session,
)
# Assertions
@@ -261,8 +268,10 @@ async def test_send_external_dispatch_multiple_endpoints_second_success(
mocked_responses.post(endpoints[0], status=404)
mocked_responses.post(endpoints[1], status=200)
+ context = self.alice._build_context()
+
# Perform the actual operation
- result = await self.context.send(self.clyde.address, msg)
+ result = await context.send(self.clyde.address, msg)
# Define the expected message status
exp_msg_status = MsgStatus(
@@ -270,7 +279,7 @@ async def test_send_external_dispatch_multiple_endpoints_second_success(
detail="Message successfully delivered via HTTP",
destination=self.clyde.address,
endpoint=endpoints[1],
- session=self.context.session,
+ session=context.session,
)
# Assertions
@@ -288,8 +297,10 @@ async def test_send_external_dispatch_multiple_endpoints_failure(
mocked_responses.post(endpoints[0], status=404)
mocked_responses.post(endpoints[1], status=404)
+ context = self.alice._build_context()
+
# Perform the actual operation
- result = await self.context.send(self.clyde.address, msg)
+ result = await context.send(self.clyde.address, msg)
# Define the expected message status
exp_msg_status = MsgStatus(
@@ -297,7 +308,7 @@ async def test_send_external_dispatch_multiple_endpoints_failure(
detail="Message delivery failed",
destination=self.clyde.address,
endpoint="",
- session=self.context.session,
+ session=context.session,
)
# Assertions