article_icon
article

Microagents - Booking Protocol

A guide helping you create a Microagent helping you to book a table at a restaurant.

2023-06-080 min readjosh

In this guide I will illustrate how to set up the code for a protocol for our microagent to query the state of a table at a restaurant and possibly book it if available. First of all, create a script and name it booking_protocol.py.

code-icon
code-icon
touch booking_protocol.py

Let's then create our two agents: customer and restaurant. These 2 agents interact in the following way:

  1. Customer: every 3 seconds queries the restaurant about availability of table 42.
  2. Restaurant: upon receiving table 42 query, checks storage for its availability (RESERVED or FREE) and confirms the status back to the user.
  3. Customer: upon receiving table status, if it is FREE, tries to book the table, (otherwise is disappointed).
  4. Restaurant: upon receiving a booking request for table 42, the restaurant books the table if it is available. It then sends booking confirmation (successful or unsuccessful) back to the user.
  5. Customer: upon receiving booking confirmation (successful or unsuccessful), logs it on the terminal.

We are now ready to write the code for our example. We need to consider each step above separately, but first, we need to define the different classes for messages to be exchanged between the customer and restaurant agents.

Messages

code-icon
code-icon
from enum import Enum

from uagents import Agent, Bureau, Context, Model

class TableStatus(str, Enum):
    RESERVED = "reserved"
    FREE = "free"

class QueryTableRequest(Model):
    table_number: int

class QueryTableResponse(Model):
    status: TableStatus

class BookTableRequest(Model):
    table_number: int

class BookTableResponse(Model):
    success: bool

We import the enum class from the Enum module. Enum is a class in python for creating enumerations, which are a set of symbolic names bound to unique, constant values.

We then import the custom classes Agent, Bureau, Context, Model from the uagents library.

We then proceed by defining the following classes for messages to be exchanged between our Microagents:

  • TableStatus: this is an enumeration that defines two possible values: RESERVED and FREE for the table at the restaurant.

  • QueryTableRequest: It has one attribute, table_number, which is an integer. This class is used to define a message that can be sent from a customer agent to a restaurant agent to query the availability of a table.

  • QueryTableResponse: It has one attribute, status, which is of type TableStatus. This class is used to define a message that can be sent from a restaurant agent to a customer agent to confirm the availability of a table.

  • BookTableRequest: It has one attribute, table_number, which is an integer. This class is used to define a message that can be sent from a customer agent to a restaurant agent to request to book a table.

  • BookTableResponse: It has one attribute, success, which is a boolean. This class is used to define a message that can be sent from a restaurant agent to a customer agent to confirm whether the table booking was successful or not.

Now, we are ready to create our two Microagent instances of the class Agent: Customer and Restaurant.

code-icon
code-icon
customer = Agent(name = "Customer")
restaurant = Agent(name = "Restaurant")

We then proceed by defining each agent's behavior and functions to be executed. We first consider the customer agent then the restaurant agent.

Customer

code-icon
code-icon
@customer.on_interval(period=3.0, messages=QueryTableRequest)
async def interval(ctx: Context):
    started = ctx.storage.get("started")

    if not started:
        await ctx.send(restaurant.address, QueryTableRequest(table_number=42))

    ctx.storage.set("started", True)
  • @customer.on_interval(period=3.0, messages=QueryTableRequest): This defines the on_interval decorator provided by the uagents library to schedule a function to be called every 3 seconds. The messages parameter specifies the type of message that the function should handle, in this case a QueryTableRequest. The ctx parameter is an instance of the Context class which provides access to various resources and methods that are available to the agent. The function starts by checking if the agent has already sent a request to the restaurant by getting the value started from the agent's storage using ctx.storage.get(). If the value of started is False, then the function sends a QueryTableRequest message to the restaurant using ctx.send() method and sets the value of started to True using ctx.storage.set().

The purpose of this decorator is to have the user agent sending a query message to the restaurant agent every 3 seconds to query for the availability of table number 42. The started key is used to ensure that the query message is only sent once when the agent starts running.

code-icon
code-icon
@customer.on_message(QueryTableResponse, replies=BookTableRequest)
async def handle_query_response(ctx: Context, _sender: str, msg: QueryTableResponse):
    if msg.status == TableStatus.FREE:
        ctx.logger.info("Table is free, attempting to book it now")
        await ctx.send(restaurant.address, BookTableRequest(table_number=42))
    else:
        ctx.logger.info("Table is not free - nothing more to do")
  • @user.on_message(QueryTableResponse, replies=BookTableRequest): This defines a message handler for the customer agent that handles incoming QueryTableResponse messages from the restaurant agent. If the response indicates that the table is free, it sends a BookTableRequest message to the restaurant agent to attempt to book the table with the specified number (i.e. 42). If the response indicates that the table is not free, then it logs a message indicating that there is nothing more to do.
code-icon
code-icon
@customer.on_message(BookTableResponse, replies=set())
async def handle_book_response(ctx: Context, _sender: str, msg: BookTableResponse):
    if msg.success:
        ctx.logger.info("Table reservation was successful")
    else:
        ctx.logger.info("Table reservation was UNSUCCESSFUL")
  • @customer.on_message(BookTableResponse, replies=set()): This defines a message handler that handles a BookTableResponse message. It takes in a Context object, the name of the sender of the message, and the message itself. The function first checks if the reservation was successful by looking at the success attribute of the message. If it was successful, the function logs a message at the INFO level saying that the table reservation was successful. If it was unsuccessful, the function logs a message at the INFO level saying that the table reservation was unsuccessful.

Restaurant

code-icon
code-icon
@restaurant.on_message(model=QueryTableRequest, replies=QueryTableResponse)
async def handle_query_request(ctx: Context, sender: str, msg: QueryTableRequest):
    if ctx.storage.has(str(msg.table_number)):
        status = TableStatus.RESERVED
    else:
        status = TableStatus.FREE

    ctx.logger.info(f"Table {msg.table_number} query. Status: {status}")

    await ctx.send(sender, QueryTableResponse(status=status))
  • @restaurant.on_message(model=QueryTableRequest, replies=QueryTableResponse): This defines a message handler which handles a QueryTableRequest message from a customer agent. It takes in a Context object, the name of the sender of the message, and the message itself. It first checks if the requested table number is in the storage by calling the has method on the ctx.storage object. If the table number is in the storage, the status is set to TableStatus.RESERVED. Otherwise, the status is set to TableStatus.FREE. The function then logs a message at the INFO level, indicating the table number and its status. Finally, the function sends a QueryTableResponse message back to the sender with the status of the table. Note that the replies parameter specifies the expected response message type, which is QueryTableResponse. This helps the system to verify that the response message has the correct format.
code-icon
code-icon
@restaurant.on_message(model=BookTableRequest, replies=BookTableResponse)
async def handle_book_request(ctx: Context, sender: str, msg: BookTableRequest):
    if ctx.storage.has(str(msg.table_number)):
        success = False
    else:
        success = True
        ctx.storage.set(str(msg.table_number), sender)

    await ctx.send(sender, BookTableResponse(success=success))
  • @restaurant.on_message(model=BookTableRequest, replies=BookTableResponse): This handles a BookTableRequest message. It takes in a Context object, the name of the sender of the message, and the message itself. It first checks if the requested table number is in the storage by calling the has method on the ctx.storage object. If the table number is in the storage, it means that the table is already reserved and the success variable is set to False. Otherwise, the function sets the success variable to True to indicate that the table has been successfully reserved, and stores the name of the sender in the storage using the set method. The function then sends a BookTableResponse message back to the sender with the result of the table booking operation. If the reservation was successful, the success field of the response will be True. Otherwise, it will be False. Note that the replies parameter specifies the expected response message type, which is BookTableResponse. This helps the system to verify that the response message has the correct format.

Bureau

We have defined both of our agents and their respective behaviors and functions. Since we have multiple agents in this example we add them to a bureau, so for them to be run at the same time.

code-icon
code-icon
bureau = Bureau()
bureau.add(user)
bureau.add(restaurant)

if __name__ == "__main__":
    bureau.run()

The overall code for this example should look as follows:

code-icon
code-icon
from enum import Enum

from uagents import Agent, Bureau, Context, Model

class TableStatus(str, Enum):
    RESERVED = "reserved"
    FREE = "free"

class QueryTableRequest(Model):
    table_number: int

class QueryTableResponse(Model):
    status: TableStatus

class BookTableRequest(Model):
    table_number: int

class BookTableResponse(Model):
    success: bool

user = Agent(name="user")
restaurant = Agent(name="restaurant")

@customer.on_interval(period=3.0, messages=QueryTableRequest)
async def interval(ctx: Context):
    started = ctx.storage.get("started")

    if not started:
        await ctx.send(restaurant.address, QueryTableRequest(table_number=42))

    ctx.storage.set("started", True)

@customer.on_message(QueryTableResponse, replies=BookTableRequest)
async def handle_query_response(ctx: Context, _sender: str, msg: QueryTableResponse):
    if msg.status == TableStatus.FREE:
        ctx.logger.info("Table is free, attempting to book it now")
        await ctx.send(restaurant.address, BookTableRequest(table_number=42))
    else:
        ctx.logger.info("Table is not free - nothing more to do")

@customer.on_message(BookTableResponse, replies=set())
async def handle_book_response(ctx: Context, _sender: str, msg: BookTableResponse):
    if msg.success:
        ctx.logger.info("Table reservation was successful")
    else:
        ctx.logger.info("Table reservation was UNSUCCESSFUL")

@restaurant.on_message(model=QueryTableRequest, replies=QueryTableResponse)
async def handle_query_request(ctx: Context, sender: str, msg: QueryTableRequest):
    if ctx.storage.has(str(msg.table_number)):
        status = TableStatus.RESERVED
    else:
        status = TableStatus.FREE

    ctx.logger.info(f"Table {msg.table_number} query. Status: {status}")

    await ctx.send(sender, QueryTableResponse(status=status))

@restaurant.on_message(model=BookTableRequest, replies=BookTableResponse)
async def handle_book_request(ctx: Context, sender: str, msg: BookTableRequest):
    if ctx.storage.has(str(msg.table_number)):
        success = False
    else:
        success = True
        ctx.storage.set(str(msg.table_number), sender)
    await ctx.send(sender, BookTableResponse(success=success))

bureau = Bureau()
bureau.add(customer)
bureau.add(restaurant)

Run the script

To run the script, go on your terminal, make sure you are in the right directory for your Microagent project and that you activated your virtual environment. Run the script:

code-icon
code-icon
python booking_protocol.py

You should get the following output:

We successfully created two Microagents, Customer and Restaurant, with the customer agent querying for availability of table 42 at the restaurant and possibly book it. The restaurant agent, in turn, checks for availability of the table and answers back with the status of such booking request. In our case, booking is successful.


More from Fetch