def run_multi_agent(config, num_dialogues, num_agents):
        """
        This function will create multiple conversational agents and
        orchestrate the conversation among them.

        Note: In Plato v. 0.1 this function will create two agents.

        :param config: a dictionary containing settings
        :param num_dialogues: how many dialogues to run for
        :param num_agents: how many agents to spawn
        :return: some statistics
        """

        conv_sys_agents = []
        conv_user_agents = []
        objective_success = 0

        generic_agents = False
        if 'GENERAL' in config and 'generic' in config['GENERAL'] \
                and config['GENERAL']['generic']:
            generic_agents = bool(config['GENERAL']['generic'])

        # Verify that we have a DM section for each agent in the config and
        # initialize agents
        for a in range(num_agents):
            ag_id_str = 'AGENT_' + str(a)
            if 'role' in config[ag_id_str]:
                if config[ag_id_str]['role'] == 'system':
                    if generic_agents:
                        conv_sys_agents.append(
                            ConversationalGenericAgent(config, a))
                    else:
                        conv_sys_agents.append(
                            ConversationalMultiAgent(config, a))

                elif config[ag_id_str]['role'] == 'user':
                    if generic_agents:
                        conv_user_agents.append(
                            ConversationalGenericAgent(config, a))
                    else:
                        conv_user_agents.append(
                            ConversationalMultiAgent(config, a))

                else:
                    print('WARNING: Unknown agent role: {0}!'
                          .format(config[ag_id_str]['role']))
            else:
                raise ValueError('Role for agent {0} not defined in config.'
                                 .format(a))

        # TODO: WARNING: FOR NOW ASSUMING WE HAVE ONE SYSTEM AND ONE USER AGENT
        conv_user_agents[0].initialize()

        conv_sys_agents[0].initialize()

        for dialogue in range(num_dialogues):
            print('\n=====================================================\n\n'
                  'Dialogue %d (out of %d)\n' % (dialogue + 1, num_dialogues))

            # WARNING: FOR NOW ASSUMING WE HAVE ONE SYSTEM AGENT.
            user_output = conv_user_agents[0].start_dialogue()
            sys_output = conv_sys_agents[0].start_dialogue(
                {'goal': user_output['goal']}
            )

            while not all(ca.terminated() for ca in conv_sys_agents) \
                    and not all(ca.terminated() for ca in conv_user_agents):
                # WARNING: FOR NOW ASSUMING WE HAVE ONE USER AGENT.
                user_output = conv_user_agents[0].continue_dialogue(
                    {
                        'other_input_raw': sys_output['output_raw'],
                        'other_input_dacts': sys_output['output_dacts'],
                        'goal': sys_output['goal']
                    })

                # Need to check for termination condition again, for the case
                # where the system says 'bye' and then the user says bye too.
                if all(ca.terminated() for ca in conv_sys_agents) \
                        or all(ca.terminated() for ca in conv_user_agents):
                    break

                # WARNING: FOR NOW ASSUMING WE HAVE ONE SYSTEM AGENT.
                sys_output = conv_sys_agents[0].continue_dialogue(
                    {
                        'other_input_raw': user_output['output_raw'],
                        'other_input_dacts': user_output['output_dacts'],
                        'goal': user_output['goal']
                    }
                )

                # Sync goals (user has ground truth)
                conv_sys_agents[0].set_goal(conv_user_agents[0].get_goal())

            # Check if there is a goal. For example, if the agents are generic
            # there may not be a tracked goal.
            if not conv_sys_agents[0].agent_goal:
                continue

            # Consolidate goals to track objective success (each agent tracks
            # different things)

            # From the sys we keep everything except the status of actual
            # requests (filled or not)
            goal = conv_sys_agents[0].agent_goal
            goal.actual_requests = \
                conv_user_agents[0].agent_goal.actual_requests

            _, _, obj_succ = conv_sys_agents[0].reward_func.calculate(
                conv_sys_agents[0].get_state(), [], goal=goal,
                agent_role="system")

            objective_success += 1 if obj_succ else 0

            print(f'OBJECTIVE TASK COMPLETION: {obj_succ}')

            for ca in conv_sys_agents:
                ca.end_dialogue()

            for ca in conv_user_agents:
                ca.end_dialogue()

        # Collect statistics
        statistics = {}

        for i in range(num_agents):
            ag_id_str = 'AGENT_' + str(i)
            statistics[ag_id_str] = {'role': config[ag_id_str]['role']}

        statistics['AGENT_0']['dialogue_success_percentage'] = \
            100 * \
            float(conv_sys_agents[0].num_successful_dialogues / num_dialogues)
        statistics['AGENT_0']['avg_cumulative_rewards'] = \
            float(conv_sys_agents[0].cumulative_rewards / num_dialogues)
        statistics['AGENT_0']['avg_turns'] = \
            float(conv_sys_agents[0].total_dialogue_turns / num_dialogues)
        statistics['AGENT_0']['objective_task_completion_percentage'] = \
            100 * float(objective_success / num_dialogues)

        statistics['AGENT_1']['dialogue_success_percentage'] = \
            100 * \
            float(conv_user_agents[0].num_successful_dialogues / num_dialogues)
        statistics['AGENT_1']['avg_cumulative_rewards'] = \
            float(conv_user_agents[0].cumulative_rewards / num_dialogues)
        statistics['AGENT_1']['avg_turns'] = \
            float(conv_user_agents[0].total_dialogue_turns / num_dialogues)
        statistics['AGENT_1']['objective_task_completion_percentage'] = \
            100 * float(objective_success / num_dialogues)

        print('\n\nSYSTEM dialogue Success Rate: {0}\n'
              'Average Cumulative Reward: {1}\n'
              'Average Turns: {2}'.
              format(100 *
                     float(conv_sys_agents[0].num_successful_dialogues
                           / num_dialogues),
                     float(conv_sys_agents[0].cumulative_rewards
                           / num_dialogues),
                     float(conv_sys_agents[0].total_dialogue_turns
                           / num_dialogues)))

        print('\n\nUSER dialogue Success Rate: {0}\n'
              'Average Cumulative Reward: {1}\n'
              'Average Turns: {2}'.
              format(100 *
                     float(conv_user_agents[0].num_successful_dialogues
                           / num_dialogues),
                     float(conv_user_agents[0].cumulative_rewards
                           / num_dialogues),
                     float(conv_user_agents[0].total_dialogue_turns
                           / num_dialogues)))

        avg_rew = 0.5 * (
                float(conv_sys_agents[0].cumulative_rewards / num_dialogues) +
                float(conv_user_agents[0].cumulative_rewards / num_dialogues))
        print(f'\n\nAVERAGE rewards: {avg_rew}')

        print('\n\nObjective Task Success Rate: {0}'.format(
            100 * float(objective_success / num_dialogues)))

        return statistics

        This function will create multiple conversational agents and
        orchestrate the conversation among them.

        Note: In Plato v. 0.1 this function will create two agents.

        :param config: a dictionary containing settings
        :param num_dialogues: how many dialogues to run for
        :param num_agents: how many agents to spawn
        :return: some statistics
        
plato/controller/basic_controller.py