Step-by-step agent negotiation

In this section, you will see how the negotiation works in our framework.

The big picture

The following diagram gives you a high-level understanding on how a generic negotiation works.

sequenceDiagram participant Agent_1 participant Agent_2 participant Controller participant OEF activate Controller Agent_1->>Agent_1: (1) get_service_description() Agent_1->>OEF: (2) register_service(description) Agent_2->>Agent_2: (3) get_service_description() Agent_2->>OEF: (4) register_service(description) Agent_1->>Agent_1: (5) build_services_query() Agent_1->>OEF: (6) search_services(query) OEF->>Agent_1: (7) search_results(agents) Agent_1->>Agent_2: (8) send_cfp(1, 1, "agent_2_pbk", 0, query) Agent_2->>Agent_2: (9) get_proposal() Agent_2->>Agent_1: (10) send_propose(2, 1, "agent_1_pbk", 1, proposals) Agent_1->>Agent_2: (11) send_accept(3, 1, "agent_2_pbk", 2) Agent_2->>Agent_1: (13) send_accept(4, 1, "agent_1_pbk", 3) Agent_2->>Controller: (14) send_message(4, 1, "controller_pbk", transaction) Agent_1->>Controller: (12) send_message(5, 1, "controller_pbk", transaction) Controller->>Agent_1: (15) TransactionConfirmation(transaction) Controller->>Agent_2: (16) TransactionConfirmation(transaction) deactivate Controller

Let’s see step by step what happens:

  1. Agent_1 calls get_service_description(is_supply) to generate the service description. is_supply is a flag to switch between registering goods which the agent supplies (is_supply is True, the agent is in a seller role for these goods) and registering goods which the agent demands (is_supply is False, the agent is in a buyer role for these goods).

  2. Agent_1 sends a register_service request to the OEF node, to register her services (the goods she supplies/demands) on the OEF.

  3. Analogous to (1), but for Agent_2

  4. Analogous to (2), but for Agent_2

  5. Agent_1 calls build_services_query(is_searching_for_sellers) to generate a query for the OEF. is_searching_for_sellers is a flag to switch between searching for sellers and searching for buyers of the goods referenced in the query. If the agent is searching for sellers than the agent is in the buyer role, similarly when searching for buyers the agent is in a seller role.

  6. Agent_1 send a search_service request with the query previously generated.

  7. The OEF node returns a search result with the list of agent ids matching the query

  8. Agent_1 finds Agent_2, so Agent_1 sends a CFP to Agent_2, meaning that she wants to start a negotiation. The CFP contains a reference to the goods which Agent_1 is interested in and whether Agent_1 is a buyer or seller of these goods, both in the form of the query.

  9. Agent_2 calls get_proposal() to generate a proposal for answering Agent_1

  10. Agent_2 replies with a Propose message as an answer for the CFP.

  11. Agent_1 sends an Accept message to Agent_2, meaning that she accepts the proposal.

  12. Agent_2 replies with a matched accept to Agent_1, meaning that she confirms definitively the transaction.

  13. Agent_2 sends a Transaction request to the Controller (analogous to step 12).

  14. Agent_1 sends a Transaction request to the Controller.

  15. The Controller notifies Agent_1 that the transaction has been confirmed.

  16. The Controller notifies Agent_2 that the transaction has been confirmed.

Notice that this is the behaviour of the BaselineAgent. By modifying the default strategy, you can change the behaviour in steps 1 (or 3), 5 and 9. The other methods are handled by our implementation of the FIPA negotiation protocol.

Analyzing the APIs

In the following, we’re going to describe the steps listed before, but more in detail, using code examples from the framework.

Instantiate an agent

[1]:
from tac.agents.participant.v1.examples.baseline import BaselineAgent
from tac.agents.participant.v1.examples.strategy import BaselineStrategy

strategy = BaselineStrategy()
agent = BaselineAgent(name="tac_agent", oef_addr="127.0.0.1", oef_port=10000, strategy=strategy)

Registration

This part covers the steps 1-4. That is, when the agents build their own description and register their service to the OEF. This step allows the agents to be found via search queries, and hence increasing the probability to be found by other agents.

The get_service_description(is_supply) method

This method generates a Description object of the Python OEF SDK (check the documentation here). It is basically a data structure that holds a dictionary objects, mapping from attribute names (strings) to some values. Moreover, it might refer to a DataModel object, which defines the abstract structure that a Description object should have. You can think of them in terms of the relational database domain: a DataModel object corresponds to an SQL Table, whereas a Description object correspond to a row of that table.

The method is used in steps 1 and 3 by Agent_1 and Agent_2, respectively.

In the context of TAC, the Description for service registration looks like the following:

[ ]:
from oef.schema import Description


description = Description({
    'tac_good_0_pbk': 1,
    'tac_good_1_pbk': 1,
    'tac_good_2_pbk': 1,
    'tac_good_3_pbk': 1,
    'tac_good_4_pbk': 1,
    'tac_good_5_pbk': 1,
    'tac_good_6_pbk': 1,
    'tac_good_7_pbk': 1,
    'tac_good_8_pbk': 1,
    'tac_good_9_pbk': 1
}, data_model=None)

The argument data_model is set to None, but in the framework it is properly set depending on the context That is, when we refer to a description of an agent in the seller role, we use the "tac_supply" data model (the agent supplies goods), whereas in the case of a description of an agent in the buyer role, we use the "tac_demand" data model (the agent demands goods).

The attribute names tac_good_X_pbk is the name given to each tradable good.
Notice that the keys are automatically generated, depending on the number of goods in the game.

Depending on the value of the flag is_supply, the generated description contains different quantities for each good:

  • If is_supply is True, then the quantities good are generated by the method Strategy.supplied_good_quantities(current_holdings) and have to be interpreted as the amount of each good the agent is willing to sell;

  • If is_supply is False, then the quantities good are generated by the method Strategy.demanded_good_quantities(current_holdings) and have to be interpreted as the amount of each good the agent is willing to buy;

Notice that supplied_good_quantities and demanded_good_quantities are user-defined method to be implemented in the Strategy object, which defines the agent’s behaviour.

Here you can see the output of BaselineStrategy.supplied_good_quantities and BaselineStrategy.demanded_good_quantities

[ ]:
from tac.agents.participant.v1.examples.strategy import BaselineStrategy
baseline_strategy = BaselineStrategy()

current_holdings = [2, 3, 4, 1]

supplied_good_quantities = baseline_strategy.supplied_good_quantities(current_holdings)
demanded_good_quantities = baseline_strategy.demanded_good_quantities(current_holdings)

print("Supplied quantities: ", supplied_good_quantities)
print("Demanded quantities: ", demanded_good_quantities)

The baseline supplied quantities are the current holdings minus 1. This is because the first quantity is the most valuable one in terms of utility, due to the logarithmic shape of the Cobb-Douglas utility function

The baseline demanded quantities are just 1 for every good. this is because every good instance is going to be providing additional utility to the agent, due to the ever-increasing utility function.

However, the baseline strategy is relatively simple and naive, so you might think to more complex and/or dynamic computation of supplied/demanded quantities, which affects the your agent’s behaviour during the whole competition.

The register_service(description) method

The register_service(description) method is implemented the OEF Python SDK. You can find the informal introduction to the registering and advertising processes, and the reference documentation of the API here.

The method is used in steps 2 and 4 by Agent_1 and Agent_2 respectively.

Searching

This part covers the steps 5-7 of the diagram.

The searching/advertising features of the OEF platform are crucial in the TAC, since they allow the discovery of potential sellers or buyers.

The build_services_query(is_searching_for_sellers) method

The build_services_query(is_searching_for_sellers) method returns a Query object that is used for searching, on the OEF platform, potential agents to negotiate with. The method takes in input the flag is_searching_for_sellers that determines whether the generated query should search for buyer or sellers.

More detail and code examples about how to build a query in the OEF Python SDK can be found here

Depending on the value of the flag is_searching_for_sellers, the generated description contains different quantities for each good:

  • If is_searching_for_sellers is True, then the good public keys are generated by the method Strategy.demanded_good_pbks(good_pbks, current_holdings) and have to be interpreted as the goods the agent is willing to buy;

  • If is_searching_for_sellers is False, then the good public keys are generated by the method Strategy.supplied_good_pbks(good_pbks, current_holdings) and have to be interpreted as the goods the agent is willing to sell;

Notice that demanded_good_pbks and supplied_good_pbks are user-defined method to be implemented in the Strategy object, which defines the agent’s behaviour.

Here you can see the output of BaselineStrategy.supplied_good_pbks and BaselineStrategy.demanded_good_pbks

[ ]:

from tac.agents.participant.v1.examples.strategy import BaselineStrategy
baseline_strategy = BaselineStrategy()

good_pbks = ["tac_good_0_pbk", "tac_good_1_pbk", "tac_good_2_pbk", "tac_good_3_pbk"]
current_holdings = [2, 3, 4, 1]

supplied_good_pbks = baseline_strategy.supplied_good_pbks(good_pbks, current_holdings)
demanded_good_pbks = baseline_strategy.demanded_good_pbks(good_pbks, current_holdings)

print("Supplied good public keys: ", supplied_good_pbks)
print("Demanded good public keys: ", demanded_good_pbks)


As you can notice, the baseline supplied goods are the ones for which the holdings are strictly greater than 1, whereas the baseline demanded goods are all the goods.

You can control what goods your agent is looking for during the competition by modifying those methods.

The search_services(search_id, query) method

The `search_services(search_id, query) <http://oef-sdk-docs.fetch.ai/oef.html#oef.agents.Agent.search_services>`__ method is used send a search request to the OEF node. The OEF node will search for registered agents in the service directory, and the ones whose description matches the query will be included in the search result (see below).

For further details, look here.

The on_search_result(agents) method

The `on_search_result(agents) <http://oef-sdk-docs.fetch.ai/oef.html?#oef.agents.Agent.on_search_result>`__ method is a callback that it is called when the agent receives a search result from the OEF node.

It contains a list of agent identifiers that satisfy the search criteria of the corresponding search request.

Negotiation

This part covers the steps 8-14 of the diagram.

Further details of a generic negotiation in the OEF platform can be found here and here

Call for proposals

The message that initiates a negotiation is called “Call for proposals”, or CFP. A CFP message contains a query object which defines what the agent is looking for.

The get_proposals() method and Propose message

The Strategy.get_proposals() method defines how an agent replies to the incoming CFPs. The output of this method is a list of Description objects.

Here’s an example of output:

[ ]:
from tac.agents.participant.v1.examples.strategy import BaselineStrategy
baseline_strategy = BaselineStrategy()
proposals = baseline_strategy.get_proposals(
    good_pbks=["tac_good_0_pbk", "tac_good_1_pbk"],
    current_holdings=[2, 2],
    utility_params=[0.4, 0.6],
    tx_fee=0.1,
    is_seller=True,
    world_state=False
)

print(proposals[0].values)

The values of the Description dictionary are the good quantities plus a field "price" that specifies the price of the set of goods proposed.

The generated proposals in step 9 are then sent in a Propose message to the agent that initiated the negotiation (step 10).

Notice that get_proposals() is an abstract method of the Strategy object, which hence it’s another way to modify the behaviour of the agent.

The Accept message

If the proposal is profitable, the agent that receives a Propose (in the example Agent_1) can reply with an Accept message, which means that she accepts the offer (step 11)

Transaction request

Alongside the Accept message, the agent also sends a transaction request to the Controller agent (step 12). The controller then waits until also the counterparty sends the request for the same transaction.

The Matched Accept

when the other agent (in the example, Agent_2) receives an Accept, she replies with another accept, that we call “matched accept” (step 13). That is, a notification for Agent_1 that she acknowledged the Agent_1’s acceptance.

At the same time, Agent_2 also sends a transaction request in step 14 (analogous to step 12).

Transaction confirmations

Once the Controller received the transaction requests from both the involved parties, he stores the transaction in the ledger and sends back a TransactionConfirmation message to both the agents to let them update their internal state.