Exemplo n.º 1
0
def load_plugin_directory(plugin_dir):
    """
    Will crawl Eva's plugin directory and load the plugin info files for all the
    valid plugins found. The info file information will eventually be used when
    enabling plugins and their dependencies.

    This function does not return anything. It stores all plugin information in
    the ``conf['plugins']`` dict. Every plugin should have the following accessible
    information once this function is run::

        conf['plugins'][<plugin_id>] = {
            'info': 'Data from the info file for this plugin'
            'path': 'The path of the plugin on disk'
            'git': 'True if the plugin is a git repo (and can be updated)'
        }

    Use the following statement to access the conf dict: ``from eva import conf``

    :param plugin_dir: The directory containing available Eva plugins. Typically
        the return value of :func:`get_plugin_directory`.
    :type plugin_dir: string
    """
    log.info('Loading plugins from: %s' % plugin_dir)
    plugins = {}
    if os.path.isdir(plugin_dir):
        for plugin_name in os.listdir(plugin_dir):
            plugin_path = plugin_dir + '/' + plugin_name
            if not os.path.isdir(plugin_path):
                # A stranded file in the plugins directory.
                log.debug('Ignoring file: %s' % plugin_path)
                continue
            if plugin_name.startswith('_') or plugin_name.endswith('_'):
                # Skip folders that start or end with '_'.
                log.debug('Skipping file: %s' % plugin_path)
                continue
            # Valid plugins must have an info file and matching py file.
            if not os.path.exists(plugin_path + '/' + plugin_name + '.info'):
                log.debug('Plugin found with no info file - skipping: %s' %
                          plugin_name)
                continue
            if not os.path.exists(plugin_path + '/' + plugin_name + '.py'):
                log.debug('Plugin found with no python file - skipping: %s' %
                          plugin_name)
                continue
            # At this point we assume we have a valid plugin.
            # Fetch plugin info.
            plugins[plugin_name] = {
                'info': load_plugin_info(plugin_path, plugin_name),
                'path': plugin_path,
                'git': plugin_is_git_repo(plugin_path)
            }
            log.debug('Plugin info: %s' % plugins[plugin_name])
        if 'plugins' in conf:
            conf['plugins'].update(plugins)
        else:
            conf['plugins'] = plugins
    else:
        log.warning('Plugin directory does not exist - ' + plugin_dir)
Exemplo n.º 2
0
def boot():
    """
    The function that runs the Eva boot sequence and loads all the plugins.

    Fires the `eva.pre_boot` and `eva.post_boot` triggers.
    """
    log.info('Beginning Eva boot sequence')
    gossip.trigger('eva.pre_boot')
    load_plugins()
    gossip.trigger('eva.post_boot')
    log.info('Eva booted successfully')
Exemplo n.º 3
0
def get_return_data(context):
    """
    This function is used to extract appropriate data from the context object
    before sending it to the Eva clients.

    It will check the context object for a text response and an audio response,
    and return a dict containing this information.

    :param context: The context object used for this interaction.
    :type context: :class:`eva.context.EvaContext`
    :return: A dict that may contain the key `output_text`, `output_audio`, or
        both. It should be identical to the return value of the :func:`interact`
        function barring any changes during the `eva.pre_return_data` trigger.
    :rtype: dict
    """
    return_data = {}
    if context.get_output_audio():
        log.info('Audio response generated')
        audio_data = {'audio': context.get_output_audio(),
                      'content_type': context.get_output_audio_content_type()}
        return_data['output_audio'] = audio_data
    else:
        log.info('This interaction yielded no output audio')
        return_data['output_audio'] = None
    if context.get_output_text():
        log.info('Response text: %s' %context.get_output_text())
        return_data['output_text'] = context.get_output_text()
    else:
        log.info('This interaction yielded no output text')
        return_data['output_text'] = None
    return return_data
Exemplo n.º 4
0
def enable_plugins():
    """
    Function that enables all plugins specified in Eva configuration file.
    Will enable all available plugins if none is specified in the configs.
    """
    log.info('Enabling plugins specified in configuration')
    to_enable = conf['eva']['enabled_plugins']
    if len(to_enable) < 1:
        log.info(
            'No plugins specified in configuration, enabling all available plugins'
        )
        to_enable = conf['plugins'].keys()
    downloadable_plugins = get_downloadable_plugins()
    for plugin_name in to_enable:
        enable_plugin(plugin_name, downloadable_plugins)
Exemplo n.º 5
0
def download_plugin(plugin_id, destination):
    """
    Will download the specified plugin to the specified destination if it is
    found in the plugin repository.

    :param plugin_id: The plugin ID to download.
    :type plugin_id: string
    :param destination: The destination to download the plugin on disk.
    :type destination: string
    """
    downloadable_plugins = get_downloadable_plugins()
    if plugin_id not in downloadable_plugins:
        log.error('Could not find plugin in repository: %s' % plugin_id)
        return
    if os.path.exists(destination): shutil.rmtree(destination)
    Repo.clone_from(downloadable_plugins[plugin_id]['url'], destination)
    log.info('%s plugin downloaded' % plugin_id)
Exemplo n.º 6
0
def publish(message, channel='eva_messages'):
    """
    A helper function used to broadcast messages to all available Eva clients.

    :todo: Needs to be thoroughly tested (especially with audio data).
    :param message: The message to send to clients.
    :type message: string
    :param channel: The channel to publish in. The default channel that clients
        should be listening on is called 'eva_messages'.
    :type channel: string
    """
    log.info('Ready to publish message')
    gossip.trigger('eva.pre_publish', message=message)
    pubsub = get_pubsub()
    log.info('Publishing message: %s' %message)
    gossip.trigger('eva.publish', message=message)
    pubsub.publish(channel, message)
    gossip.trigger('eva.post_publish', message=message)
Exemplo n.º 7
0
def load_plugins():
    """
    The function that is called during Eva's boot sequence.
    Will fetch the plugin directory , load all of the plugins' info files, load
    all of the plugin's configurations, and enable all the required plugins and
    their dependencies specified in Eva's configuration file.

    Fires the `eva.plugins_loaded` trigger.
    """
    # Get all plugins.
    plugin_dir = get_plugin_directory()
    load_plugin_directory(plugin_dir)
    # Get all user-defined configurations.
    config_dir = conf['eva']['config_directory']
    if '~' in config_dir: config_dir = os.path.expanduser(config_dir)
    load_plugin_configs(config_dir)
    # Enable all necessary plugins.
    enable_plugins()
    gossip.trigger('eva.plugins_loaded')
    log.info('Plugins loaded successfully')
Exemplo n.º 8
0
def load_plugin_configs(config_dir):
    """
    Function that loops through all available plugins and loads their
    corresponding plugin configuration if found in the configuration directory
    provided.

    The :func:`load_plugin_directory` function must be called before calling this
    function as it relies on the plugin info files having been loaded into the
    ``conf['plugins']`` dict.

    :param config_dir: The configuration directory that holds all Eva plugin
        configuration files.
    :type config_dir: string
    """
    # Loop through plugins and fetch configs.
    log.info('Loading plugin configuration files from %s' % config_dir)
    if 'plugins' not in conf:
        log.warning('No plugin configurations loaded')
        return
    for plugin in conf['plugins']:
        plugin_config = get_plugin_config(plugin, config_dir)
        conf['plugins'][plugin]['config'] = plugin_config
        log.debug('Loaded plugin configuration for %s: %s' %
                  (plugin, plugin_config))
Exemplo n.º 9
0
def interact(data):
    """
    Eva's bread and butter function. Feeding data from the clients directly to
    this function will return a response dict, ready to be consumed by the
    clients as a response. This takes care of firing all the necessary triggers
    so that the plugins get a say in the responding text and/or audio.

    Fires the following triggers:
        * `eva.voice_recognition`
        * `eva.pre_interaction_context`
        * `eva.pre_interaction`
        * `eva.interaction`
        * `eva.post_interaction`
        * `eva.text_to_speech`
        * `eva.pre_return_data`

    :param data: The data received from the clients on query/command.
        See :func:`eva.context.EvaContext.__init__` for more details.
    :type data: dict
    :return: A dictionary with all the information necessary for the clients to
        handle the response appropriately. Typically something like this::

            dict {
                'output_text': The text of the response from Eva
                'output_audio': dict {
                    'audio': The binary audio data of the response (optional)
                    'content_type': The content type of the audio binary data (optional)
                }
            }

    :rtype: dict
    """
    log.info('Starting eva interaction')
    if 'input_text' in data:
        log.info('Interaction text provided: %s' %data['input_text'])
    if 'input_audio' in data:
        log.info('Interaction audio provided')
        if 'input_text' not in data:
            gossip.trigger('eva.voice_recognition', data=data)
    gossip.trigger('eva.pre_interaction_context', data=data)
    context = EvaContext(data)
    gossip.trigger('eva.pre_interaction', context=context)
    gossip.trigger('eva.interaction', context=context)
    gossip.trigger('eva.post_interaction', context=context)
    # Handle text-to-speech opportunity.
    if context.get_output_text() and not context.get_output_audio():
        gossip.trigger('eva.text_to_speech', context=context)
    # Prepare return data.
    return_data = get_return_data(context)
    # One last chance to modify the return data before sending to client.
    gossip.trigger('eva.pre_return_data', return_data=return_data)
    return return_data
Exemplo n.º 10
0
def enable_plugin(plugin_id, downloadable_plugins=None):
    """
    Enables a single plugin, which entails:

        * If already enabled, return
        * If plugin not found, search online repository
        * Download if found in repository, else log and return
        * Recusively enable dependencies if found, else log error and return
        * Run a ``pip install -r requirements.txt --user`` if requirements file found
        * Insert plugin directory in Python path and dynamically import module
        * Execute the ``<plugin>.on_enable()`` function if found

    :todo: Need to clean up, comment, and shorten this function.
    :param plugin_id: The plugin id to enable.
    :type plugin_id: string
    :param downloadable_plugins: A dict of plugins that are available for
        download from Eva's repository. This is typically the return value of the
        :func:`get_downloadable_plugins` function.
    :type downloadable_plugins: dict
    """
    if plugin_enabled(plugin_id): return
    log.debug('Attempting to enable %s' % plugin_id)
    if downloadable_plugins is None:
        downloadable_plugins = get_downloadable_plugins()
    if 'plugins' not in conf: conf['plugins'] = {}
    if plugin_id not in conf['plugins']:
        if plugin_id not in downloadable_plugins:
            log.error(
                'Could not enable plugin %s: plugin not found locally or in repository'
                % plugin_id)
            return
        destination = get_plugin_directory() + '/' + plugin_id
        download_plugin(plugin_id, destination)
        conf['plugins'][plugin_id] = {
            'info': load_plugin_info(destination, plugin_id),
            'path': destination,
            'git': True
        }
        plugin_config_dir = os.path.expanduser(conf['eva']['config_directory'])
        conf['plugins'][plugin_id]['config'] = get_plugin_config(
            plugin_id, plugin_config_dir)
    plugin_conf = conf['plugins'][plugin_id]
    dependencies = plugin_conf['info']['dependencies']
    local_plugins = conf['plugins'].keys()
    available_plugs = local_plugins + list(downloadable_plugins.keys())
    # Don't bother enabling if we can't find all dependencies.
    missing_deps = set(dependencies) - set(available_plugs)
    if len(missing_deps) > 0:
        log.error('Could not import plugin ' + plugin_id +
                  ' due to unmet dependencies - ' + ', '.join(missing_deps))
        return
    # Enable dependencies.
    for dependency in dependencies:
        log.debug('Enabling %s dependency: %s' % (plugin_id, dependency))
        enable_plugin(dependency)
    # Install any python module dependencies specified by the plugin.
    plugin_path = conf['plugins'][plugin_id]['path']
    requirements_file = plugin_path + '/requirements.txt'
    if os.path.isfile(requirements_file):
        log.info(
            'Found requirements.txt for %s. Installing python dependencies' %
            plugin_id)
        pip.main(['install', '-r', requirements_file, '--user', '-qq'])
    # Do the import of our python module.
    try:
        # Let's add this directory to our path to import the module.
        if plugin_path not in sys.path: sys.path.insert(0, plugin_path)
        mod = importlib.import_module(plugin_id)
        conf['plugins'][plugin_id]['module'] = mod
        log.info('Plugin enabled: %s' % plugin_id)
        try:
            log.debug('Running %s.on_enable()' % plugin_id)
            mod.on_enable()
        except AttributeError:  # Not necessary to have a on_enable() function.
            pass
    except ImportError as err:
        log.error('Could not import plugin ' + plugin_id + ' - ' + str(err))