keiffster / program-y

Python 3.x based AIML 2.0 Chatbot interpreter, framework, related programs and knowledge files
https://keiffster.github.io/program-y/
Other
348 stars 138 forks source link

Is there a snippet for initialization of the lightweight bot with subset of AIML files? #235

Closed acriptis closed 4 years ago

acriptis commented 4 years ago

Hello,

I need to initialize AIML Brain with several AIML files (not all in storage but subset of them) and then I would like to request the method ask_question (provide a context and the question) and receive a response (if it matched).

What is the simplest way to do this? I've explored the embedded bot (https://github.com/keiffster/program-y/blob/master/src/programy/clients/embed/client.py#L24) but it requires configuration files, while I would like to pass some parameters (set of AIML files to be loaded to Brain) as arguments.

I explored some tests, like this: https://github.com/keiffster/program-y/blob/master/test/programytest/test_bot.py#L381-L399

Seems it is very similar to my case, but I dont understand what is the best way to specify set of allowed AIML files.

To be clear. I need to implement kind of this snippet:

minibot = TinyAIML(aiml_files_list = ['greeting.aiml', 'farewell.aiml'], configs=...)
response = minibot.ask_question("Hello", userid="123")

Could you point to some examples in tests which may help to solve the problem or any other useful recommendations?

Thank you!

keiffster commented 4 years ago

Hi,I like the idea and have added it to the backlog. Unfortunately I wont get round to it for a few weeks due to other planned work

acriptis commented 4 years ago

@keiffster Finally I found the solution: I've inherited from BotClient the class named AIMLEmbeddedBotClient and reimplemented __init__ method (it required me to append source code root to sys.path to enable search of components and to change current working directory to the directory of config file because config files specify relative paths).

Another case is logging, I can not find the place but somewhere logging becomes = 10, when there is no config for logging (and this breaks the logging initialization). Do you know where parse_arguments produces _logging=10? (I've missed it in debug).

So finally it is quite dirty snippet, but it works:

import sys
import os
from programy.config.file.yaml_file import YamlConfigurationFile
from programy.config.programy import ProgramyConfiguration
from programy.clients.args import CommandLineClientArguments
from programy.clients.client import BotClient
from programy.utils.license.keys import LicenseKeys
from programy.utils.substitutions.substitues import Substitutions
from programy.clients.botfactory import BotFactory
from programy.clients.events.console.config import ConsoleConfiguration

class AIMLEmbeddedBotClient(BotClient):
    def __init__(self, id, config_file_path, src_root_path=None):
        """
        BotClient that can be initilized in runtime from YAML config

        WARNING this module changes CWD (Current Working Directory)!

        ProgramY has assumptions about current directories and uses environmental variables
        to specify search paths when launched from bash scripts.

        First, ProgramY uses file paths relative to config file.
        Second, ProgramY allows to specify paths to modules which are in dot notation relative to
            project root, which is different from config dir and usually placed 2 directories higher

        In this module we gather all this configurations via parameters.

        :param id: str, unique identifier of the bot
        :param config_file_path: path to the YAML config file
        :param src_root_path: sometimes YAML config path asserts that we reference to
            modules which are part of another project, and src_modules_root_path - is a path from
            which we look for specified modules. For example YAML config
                joiner:
                    classname: templatey.processors.sentence_joiner_deduplicator.SentenceJoinerDeDuplicator
            means that a class SentenceJoinerDeDuplicator will be searched from src_modules_root_path by appending dot prefixes.

        """
        self._id = id
        self._license_keys = LicenseKeys()
        self._storage = None
        self._scheduler = None
        self._email = None
        self._trigger_mgr = None
        self._configuration = None
        self._ping_responder = None

        self._questions = 0

        self._arguments = self.parse_arguments(argument_parser=None)

        # hack:
        if self._arguments._logging == 10:
            self._arguments._logging = None

        self.initiate_logging(self.arguments)

        self._subsitutions = Substitutions()
        if self.arguments.substitutions is not None:
            self._subsitutions.load_substitutions(self.arguments.substitutions)

        # specify configuration file
        self._config_filename = config_file_path

        self.load_configuration(self.arguments)
        # self.parse_configuration()

        self.load_storage()

        ##############################################################################
        # set path because config uses relative paths
        # this required so the files specified as relative paths in YAML will be interpreted
        # correctly like in the example:
        # categories_storage:
        #                   dirs: ../../storage/categories
        #                   subdirs: true
        #                   extension: .aiml
        current_dir_path = os.path.dirname(self._config_filename)
        os.chdir(current_dir_path)
        ##############################################################################

        ##############################################################################
        # to be able to find modules of such as SentenceDeduplicator
        if not src_root_path:
            src_root_path = os.path.dirname(os.path.dirname(current_dir_path))
            src_root_path += "/src"
        sys.path.append(src_root_path)
        ##############################################################################

        self._bot_factory = BotFactory(self, self.configuration.client_configuration)

        self.load_license_keys()
        self.get_license_keys()
        self._configuration.client_configuration.check_for_license_keys(self._license_keys)

        self.load_scheduler()

        self.load_renderer()

        self.load_email()

        self.load_trigger_manager()

        self.load_ping_responder()

    def get_client_configuration(self):
        return ConsoleConfiguration()

    def parse_arguments(self, argument_parser):
        client_args = CommandLineClientArguments(self, parser=None)
        return client_args

    def load_configuration(self, arguments):

        client_config = self.get_client_configuration()
        self._configuration = ProgramyConfiguration(client_config)

        yaml_file = YamlConfigurationFile()
        yaml_file.load_from_file(self._config_filename, client_config, ".")

    def process_question(self, client_context, question):
        self._questions += 1
        return client_context.bot.ask_question(client_context, question, responselogger=self)

    def handle_user_message(self, user_id, message_text):
        """Interface method to retrieve response of particular bot for a message
        from particular user"""
        client_context = self.create_client_context(user_id)
        response = self.process_question(client_context, message_text)
        return response

Then I use it in following way:

embedded_bot = AIMLEmbeddedBotClient(id="koni", config_file_path=path_to_programy_config)
resp = embedded_bot.handle_user_message(user_id="test_user",
                                       message_text="Hello I love you tell me what is your name")

Any tips and improvements to the code are appreciated!