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.
Let’s see step by step what happens:
Agent_1
callsget_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
isTrue
, the agent is in a seller role for these goods) and registering goods which the agent demands (is_supply
isFalse
, the agent is in a buyer role for these goods).Agent_1
sends aregister_service
request to the OEF node, to register her services (the goods she supplies/demands) on the OEF.Analogous to (1), but for
Agent_2
Analogous to (2), but for
Agent_2
Agent_1
callsbuild_services_query(is_searching_for_sellers)
to generate aquery
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.Agent_1
send asearch_service
request with thequery
previously generated.The OEF node returns a search result with the list of agent ids matching the
query
Agent_1
findsAgent_2
, soAgent_1
sends aCFP
toAgent_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.Agent_2
callsget_proposal()
to generate a proposal for answeringAgent_1
Agent_2
replies with aPropose
message as an answer for theCFP
.Agent_1
sends anAccept
message toAgent_2
, meaning that she accepts the proposal.Agent_2
replies with a matched accept toAgent_1
, meaning that she confirms definitively the transaction.Agent_2
sends a Transaction request to theController
(analogous to step 12).Agent_1
sends a Transaction request to theController
.The
Controller
notifiesAgent_1
that the transaction has been confirmed.The
Controller
notifiesAgent_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).
tac_good_X_pbk
is the name given to each tradable good.Depending on the value of the flag is_supply
, the generated description contains different quantities for each good:
If
is_supply
isTrue
, then the quantities good are generated by the methodStrategy.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
isFalse
, then the quantities good are generated by the methodStrategy.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
isTrue
, then the good public keys are generated by the methodStrategy.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
isFalse
, then the good public keys are generated by the methodStrategy.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.