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)
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')
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
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)
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)
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)
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')
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))
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
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))