{ "cells": [ { "cell_type": "markdown", "metadata": { "collapsed": true, "pycharm": { "name": "#%% md\n" } }, "source": [ "\n", "# Step-by-step agent negotiation\n", "\n", "\n", "In this section, you will see how the negotiation works in our framework.\n", "\n", "## The big picture\n", "\n", "\n", "The following diagram gives you a high-level understanding on how a generic negotiation works.\n", "\n", ".. mermaid:: ../diagrams/negotiation-guide/summary.mmd\n", "\n", "Let's see step by step what happens:\n", "\n", "1. `Agent_1` calls `get_service_description(is_supply)` to generate the service description. `is_supply` is a flag to \n", "switch between registering goods which the agent supplies (`is_supply` is `True`, the agent is in a seller role for these goods) \n", "and registering goods which the agent demands (`is_supply` is `False`, the agent is in a buyer role for these goods).\n", "2. `Agent_1` sends a `register_service` request to the OEF node, to register her services (the goods she supplies/demands) on the OEF.\n", "3. Analogous to (1), but for `Agent_2`\n", "4. Analogous to (2), but for `Agent_2`\n", "5. `Agent_1` calls `build_services_query(is_searching_for_sellers)` to generate a `query` for the OEF.\n", "`is_searching_for_sellers` is a flag to switch between searching for sellers and searching for buyers of the goods referenced in the query. \n", "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.\n", "6. `Agent_1` send a `search_service` request with the `query` previously generated.\n", "7. The OEF node returns a search result with the list of agent ids matching the `query`\n", "8. `Agent_1` finds `Agent_2`, so `Agent_1` sends a `CFP` to `Agent_2`, meaning that she wants to start a negotiation.\n", "The CFP contains a reference to the goods which Agent_1 is interested in and whether Agent_1 is a buyer or seller \n", "of these goods, both in the form of the query.\n", "9. `Agent_2` calls `get_proposal()` to generate a proposal for answering `Agent_1`\n", "10. `Agent_2` replies with a `Propose` message as an answer for the `CFP`.\n", "11. `Agent_1` sends an `Accept` message to `Agent_2`, meaning that she accepts the proposal.\n", "12. `Agent_2` replies with a _matched accept_ to `Agent_1`, meaning that she confirms definitively the transaction. \n", "13. `Agent_2` sends a Transaction request to the `Controller` (analogous to step 12).\n", "14. `Agent_1` sends a Transaction request to the `Controller`.\n", "15. The `Controller` notifies `Agent_1` that the transaction has been confirmed.\n", "15. The `Controller` notifies `Agent_2` that the transaction has been confirmed.\n", "\n", "Notice that this is the behaviour of the `BaselineAgent`. By modifying the default strategy, you can change\n", "the behaviour in steps 1 (or 3), 5 and 9. The other methods are handled by our implementation of the FIPA negotiation protocol.\n", "\n", "## Analyzing the APIs\n", "\n", "In the following, we're going to describe the steps listed before, but more in detail, using code examples from the framework.\n", "\n", "### Instantiate an agent\n" ] }, { "cell_type": "code", "execution_count": 1, "metadata": { "pycharm": { "is_executing": false, "name": "#%% \n" } }, "outputs": [], "source": [ "from tac.agents.participant.v1.examples.baseline import BaselineAgent\n", "from tac.agents.participant.v1.examples.strategy import BaselineStrategy\n", "\n", "strategy = BaselineStrategy()\n", "agent = BaselineAgent(name=\"tac_agent\", oef_addr=\"127.0.0.1\", oef_port=10000, strategy=strategy)" ] }, { "cell_type": "markdown", "metadata": { "pycharm": { "name": "#%% md\n" } }, "source": [ "\n", "\n", "### Registration\n", "\n", "This part covers the steps 1-4. That is, when the agents build their own description and register their service to the OEF.\n", "This step allows the agents to be found via search queries, and hence increasing the probability to be found by other agents. \n", "\n", "#### The `get_service_description(is_supply)` method\n", "\n", "This method generates a `Description` object of the [Python OEF SDK](https://github.com/fetchai/oef-sdk-python.git) \n", "(check the documentation [here](http://oef-sdk-docs.fetch.ai/oef.html#oef.schema.Description)).\n", "It is basically a data structure that holds a dictionary objects, mapping from attribute names (strings) to some values.\n", "Moreover, it might refer to a [DataModel](http://oef-sdk-docs.fetch.ai/oef.html#oef.schema.DataModel) object, which defines\n", "the abstract structure that a `Description` object should have. You can think of them in terms of the relational database\n", "domain: a `DataModel` object corresponds to an SQL Table, whereas a `Description` object correspond to a row of that table. \n", "\n", "The method is used in steps 1 and 3 by `Agent_1` and `Agent_2`, respectively.\n", "\n", "In the context of TAC, the `Description` for service registration looks like the following:\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "pycharm": { "is_executing": false, "name": "#%% \n" } }, "outputs": [], "source": [ "from oef.schema import Description\n", "\n", "\n", "description = Description({\n", " 'tac_good_0_pbk': 1, \n", " 'tac_good_1_pbk': 1, \n", " 'tac_good_2_pbk': 1, \n", " 'tac_good_3_pbk': 1, \n", " 'tac_good_4_pbk': 1, \n", " 'tac_good_5_pbk': 1, \n", " 'tac_good_6_pbk': 1, \n", " 'tac_good_7_pbk': 1, \n", " 'tac_good_8_pbk': 1, \n", " 'tac_good_9_pbk': 1\n", "}, data_model=None)\n" ] }, { "cell_type": "markdown", "metadata": { "pycharm": { "name": "#%% md\n" } }, "source": [ "\n", "The argument `data_model` is set to `None`, but in the framework it is properly set depending on the context \n", "That is, when we refer to a description of an agent in the seller role, we use the `\"tac_supply\"` data model \n", "(the agent supplies goods), whereas in the case of a description of an agent in the buyer role, we use the \n", "`\"tac_demand\"` data model (the agent demands goods).\n", " \n", "\n", "The attribute names `tac_good_X_pbk` is the name given to each tradable good. \n", "Notice that the keys are automatically generated, depending on the number of goods in the game. \n", "\n", "Depending on the value of the flag `is_supply`, the generated description contains different quantities for each good:\n", "\n", "- If `is_supply` is `True`, then the quantities good are generated by the method `Strategy.supplied_good_quantities(current_holdings)`\n", " and have to be interpreted as the amount of each good the agent is willing to sell;\n", "- If `is_supply` is `False`, then the quantities good are generated by the method `Strategy.demanded_good_quantities(current_holdings)` \n", "and have to be interpreted as the amount of each good the agent is willing to buy; \n", "\n", "Notice that `supplied_good_quantities` and `demanded_good_quantities` are user-defined method to be implemented \n", "in the `Strategy` object, which defines the agent's behaviour.\n", "\n", "Here you can see the output of `BaselineStrategy.supplied_good_quantities` \n", "and `BaselineStrategy.demanded_good_quantities`\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "pycharm": { "is_executing": false, "name": "#%% \n" } }, "outputs": [], "source": [ "from tac.agents.participant.v1.examples.strategy import BaselineStrategy\n", "baseline_strategy = BaselineStrategy()\n", "\n", "current_holdings = [2, 3, 4, 1]\n", "\n", "supplied_good_quantities = baseline_strategy.supplied_good_quantities(current_holdings)\n", "demanded_good_quantities = baseline_strategy.demanded_good_quantities(current_holdings)\n", "\n", "print(\"Supplied quantities: \", supplied_good_quantities)\n", "print(\"Demanded quantities: \", demanded_good_quantities)\n" ] }, { "cell_type": "markdown", "metadata": { "pycharm": { "name": "#%% md\n" } }, "source": [ "\n", "The baseline supplied quantities are the current holdings minus `1`. This is because\n", "the first quantity is the most valuable one in terms of utility, \n", "due to the logarithmic shape of the [Cobb-Douglas utility function](https://en.wikipedia.org/wiki/Cobb%E2%80%93Douglas_production_function#Cobb%E2%80%93Douglas_utilities)\n", "\n", "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,\n", " due to the ever-increasing utility function.\n", "\n", "However, the baseline strategy is relatively simple and naive, so you might think to more complex and/or dynamic computation\n", "of supplied/demanded quantities, which affects the your agent's behaviour during the whole competition. \n", "\n", "#### The `register_service(description)` method\n", "\n", "The `register_service(description)` method is implemented the OEF Python SDK.\n", "You can find the informal introduction to the [registering](https://docs.fetch.ai/oef/registering/) and \n", "[advertising](https://docs.fetch.ai/oef/registering/) processes, and the reference documentation \n", "of the API [here](http://oef-sdk-docs.fetch.ai/oef.html#oef.agents.Agent.register_service).\n", "\n", "The method is used in steps 2 and 4 by `Agent_1` and `Agent_2` respectively.\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "\n", "### Searching\n", "\n", "This part covers the steps 5-7 of the diagram.\n", "\n", "The searching/advertising features of the OEF platform are crucial in the TAC, since they allow the discovery \n", "of potential sellers or buyers. \n", "\n", "#### The `build_services_query(is_searching_for_sellers)` method\n", "\n", "The `build_services_query(is_searching_for_sellers)` method returns a \n", "[Query](http://oef-sdk-docs.fetch.ai/oef.html#oef.schema.Query) object that is used for searching, on the OEF platform, \n", "potential agents to negotiate with.\n", "The method takes in input the flag `is_searching_for_sellers` that determines whether the generated query should\n", "search for buyer or sellers.\n", "\n", "More detail and code examples about how to build a query in the OEF Python SDK \n", "can be found [here](https://docs.fetch.ai/oef/searching/) \n", "\n", "\n", "Depending on the value of the flag `is_searching_for_sellers`, the generated description contains different quantities for each good:\n", "\n", "- 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)`\n", " and have to be interpreted as the goods the agent is willing to buy;\n", "- 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)` \n", "and have to be interpreted as the goods the agent is willing to sell; \n", "\n", "Notice that `demanded_good_pbks` and `supplied_good_pbks` are user-defined method to be implemented \n", "in the `Strategy` object, which defines the agent's behaviour.\n", "\n", "Here you can see the output of `BaselineStrategy.supplied_good_pbks` \n", "and `BaselineStrategy.demanded_good_pbks`\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "pycharm": { "is_executing": false, "name": "#%%\n" } }, "outputs": [], "source": [ "\n", "from tac.agents.participant.v1.examples.strategy import BaselineStrategy\n", "baseline_strategy = BaselineStrategy()\n", "\n", "good_pbks = [\"tac_good_0_pbk\", \"tac_good_1_pbk\", \"tac_good_2_pbk\", \"tac_good_3_pbk\"]\n", "current_holdings = [2, 3, 4, 1]\n", "\n", "supplied_good_pbks = baseline_strategy.supplied_good_pbks(good_pbks, current_holdings)\n", "demanded_good_pbks = baseline_strategy.demanded_good_pbks(good_pbks, current_holdings)\n", "\n", "print(\"Supplied good public keys: \", supplied_good_pbks)\n", "print(\"Demanded good public keys: \", demanded_good_pbks)\n", "\n" ] }, { "cell_type": "markdown", "metadata": { "pycharm": { "name": "#%% md\n" } }, "source": [ "\n", "As you can notice, the baseline supplied goods are the ones for which the holdings are strictly greater than `1`,\n", "whereas the baseline demanded goods are all the goods. \n", "\n", "You can control what goods your agent is looking for during the competition by modifying those methods. \n", "\n", "#### The `search_services(search_id, query)` method\n", "\n", "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.\n", "The OEF node will search for registered agents in the service directory, and the ones whose description matches the `query`\n", "will be included in the search result (see below).\n", "\n", "For further details, look [here](https://docs.fetch.ai/oef/searching/).\n", "\n", "#### The `on_search_result(agents)` method\n", "\n", "The [`on_search_result(agents)`](http://oef-sdk-docs.fetch.ai/oef.html?#oef.agents.Agent.on_search_result) \n", "method is a callback that it is called when the agent receives a search result from the\n", "OEF node.\n", "\n", "It contains a list of agent identifiers that satisfy the search criteria of the corresponding search request.\n" ] }, { "cell_type": "markdown", "metadata": { "pycharm": { "name": "#%% md\n" } }, "source": [ "\n", "### Negotiation\n", "\n", "This part covers the steps 8-14 of the diagram.\n", "\n", "Further details of a generic negotiation in the OEF platform can be found [here](https://docs.fetch.ai/oef/negotiating/)\n", "and [here](https://fetchai.github.io/oef-sdk-python/user/communication-protocols.html#using-fipa-for-negotiation)\n", "\n", "#### Call for proposals\n", "\n", "The message that initiates a negotiation is called \"Call for proposals\", or `CFP`. A `CFP` message contains a query\n", "object which defines what the agent is looking for.\n", "\n", "#### The `get_proposals()` method and Propose message\n", "\n", "The `Strategy.get_proposals()` method defines how an agent replies to the incoming CFPs. The output of this method\n", "is a list of `Description` objects.\n", "\n", "Here's an example of output:\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "pycharm": { "is_executing": false, "name": "#%%\n" } }, "outputs": [], "source": [ "from tac.agents.participant.v1.examples.strategy import BaselineStrategy\n", "baseline_strategy = BaselineStrategy()\n", "proposals = baseline_strategy.get_proposals(\n", " good_pbks=[\"tac_good_0_pbk\", \"tac_good_1_pbk\"],\n", " current_holdings=[2, 2],\n", " utility_params=[0.4, 0.6],\n", " tx_fee=0.1,\n", " is_seller=True,\n", " world_state=False\n", ")\n", "\n", "print(proposals[0].values)\n" ] }, { "cell_type": "markdown", "metadata": { "pycharm": { "name": "#%% md\n" } }, "source": [ "\n", "The values of the `Description` dictionary are the good quantities plus a field `\"price\"` that \n", "specifies the price of the set of goods proposed. \n", "\n", "The generated proposals in step 9 are then sent in a `Propose` message to the agent that initiated the negotiation (step 10).\n", "\n", "Notice that `get_proposals()` is an abstract method of the `Strategy` object, which hence it's another way to \n", "modify the behaviour of the agent.\n", "\n", "\n", "#### The Accept message\n", "\n", "If the proposal is profitable, the agent that receives a `Propose` (in the example `Agent_1`) \n", "can reply with an `Accept` message, which means that she accepts the offer (step 11)\n", "\n", "\n", "#### Transaction request\n", "\n", "Alongside the `Accept` message, the agent also sends a transaction request to the `Controller` agent (step 12).\n", "The controller then waits until also the counterparty sends the request for the same transaction.\n", "\n", "#### The Matched Accept\n", "\n", "when the other agent (in the example, `Agent_2`) receives an `Accept`, she replies with another accept, that we call\n", "\"matched accept\" (step 13). That is, a notification for `Agent_1` that she acknowledged the `Agent_1`'s acceptance.\n", "\n", "At the same time, `Agent_2` also sends a transaction request in step 14 (analogous to step 12).\n", "\n", "\n", "#### Transaction confirmations\n", "\n", "Once the `Controller` received the transaction requests from both the involved parties, he stores the transaction\n", "in the ledger and sends back a `TransactionConfirmation` message to both the agents to let them update their internal\n", "state. \n" ] } ], "metadata": { "kernelspec": { "display_name": "Python 3", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.7.3" }, "pycharm": { "stem_cell": { "cell_type": "raw", "metadata": { "collapsed": false }, "source": [] } } }, "nbformat": 4, "nbformat_minor": 1 }