Source code for chatterbot.chatterbot

import logging
from typing import Union
from chatterbot.storage import StorageAdapter
from chatterbot.logic import LogicAdapter
from chatterbot.search import TextSearch, IndexedTextSearch
from chatterbot.tagging import PosLemmaTagger
from chatterbot.conversation import Statement
from chatterbot import languages
from chatterbot import utils
import spacy


[docs] class ChatBot(object): """ A conversational dialog chat bot. :param name: A name is the only required parameter for the ChatBot class. :type name: str :keyword storage_adapter: The dot-notated import path to a storage adapter class. Defaults to ``"chatterbot.storage.SQLStorageAdapter"``. :type storage_adapter: str :param logic_adapters: A list of dot-notated import paths to each logic adapter the bot uses. Defaults to ``["chatterbot.logic.BestMatch"]``. :type logic_adapters: list :param tagger: The tagger to use for the chat bot. Defaults to :class:`~chatterbot.tagging.PosLemmaTagger` :type tagger: object :param tagger_language: The language to use for the tagger. Defaults to :class:`~chatterbot.languages.ENG`. :type tagger_language: object :param preprocessors: A list of preprocessor functions to use for the chat bot. :type preprocessors: list :param read_only: If True, the chat bot will not save any input it receives, defaults to False. :type read_only: bool :param logger: A ``Logger`` object. :type logger: logging.Logger :param model: A definition used to load a large language model. Defaults to ``None``. (Added in version 1.2.7) :type model: dict :param stream: Return output as a streaming responses when a ``model`` is defined. (Added in version 1.2.7) """ def __init__(self, name, stream=False, **kwargs): self.name = name self.stream = stream self.logger = kwargs.get('logger', logging.getLogger(__name__)) storage_adapter = kwargs.get('storage_adapter', 'chatterbot.storage.SQLStorageAdapter') logic_adapters = kwargs.get('logic_adapters', [ 'chatterbot.logic.BestMatch' ]) # Check that each adapter is a valid subclass of it's respective parent utils.validate_adapter_class(storage_adapter, StorageAdapter) # Logic adapters used by the chat bot self.logic_adapters = [] self.storage = utils.initialize_class(storage_adapter, **kwargs) tagger_language = kwargs.get('tagger_language', languages.ENG) try: Tagger = kwargs.get('tagger', PosLemmaTagger) self.tagger = Tagger(language=tagger_language) except IOError as io_error: # Return a more helpful error message if possible if "Can't find model" in str(io_error): model_name = utils.get_model_for_language(tagger_language) if hasattr(tagger_language, 'ENGLISH_NAME'): language_name = tagger_language.ENGLISH_NAME else: language_name = tagger_language raise self.ChatBotException( 'Setup error:\n' f'The Spacy model for "{language_name}" language is missing.\n' 'Please install the model using the command:\n\n' f'python -m spacy download {model_name}\n\n' 'See https://spacy.io/usage/models for more information about available models.' ) from io_error else: raise io_error primary_search_algorithm = IndexedTextSearch(self, **kwargs) text_search_algorithm = TextSearch(self, **kwargs) self.search_algorithms = { primary_search_algorithm.name: primary_search_algorithm, text_search_algorithm.name: text_search_algorithm } for adapter in logic_adapters: utils.validate_adapter_class(adapter, LogicAdapter) logic_adapter = utils.initialize_class(adapter, self, **kwargs) self.logic_adapters.append(logic_adapter) preprocessors = kwargs.get( 'preprocessors', [ 'chatterbot.preprocessors.clean_whitespace' ] ) self.preprocessors = [] for preprocessor in preprocessors: self.preprocessors.append(utils.import_module(preprocessor)) # NOTE: 'xx' is the language code for a multi-language model self.nlp = spacy.blank(self.tagger.language.ISO_639_1) self.model = None if model := kwargs.get('model'): import_path = model.pop('client') self.model = utils.initialize_class(import_path, self, **model) # Allow the bot to save input it receives so that it can learn self.read_only = kwargs.get('read_only', False)
[docs] def get_response(self, statement: Union[Statement, str, dict] = None, **kwargs) -> Statement: """ Return the bot's response based on the input. :param statement: An statement object or string. :returns: A response to the input. :param additional_response_selection_parameters: Parameters to pass to the chat bot's logic adapters to control response selection. :type additional_response_selection_parameters: dict :param persist_values_to_response: Values that should be saved to the response that the chat bot generates. :type persist_values_to_response: dict """ Statement = self.storage.get_object('statement') additional_response_selection_parameters = kwargs.pop('additional_response_selection_parameters', {}) persist_values_to_response = kwargs.pop('persist_values_to_response', {}) if isinstance(statement, str): kwargs['text'] = statement if isinstance(statement, dict): kwargs.update(statement) if statement is None and 'text' not in kwargs: raise self.ChatBotException( 'Either a statement object or a "text" keyword ' 'argument is required. Neither was provided.' ) if hasattr(statement, 'serialize'): kwargs.update(**statement.serialize()) tags = kwargs.pop('tags', []) text = kwargs.pop('text') input_statement = Statement(text=text, **kwargs) input_statement.add_tags(*tags) # Preprocess the input statement for preprocessor in self.preprocessors: input_statement = preprocessor(input_statement) # Mark the statement as being a response to the previous if input_statement.in_response_to is None: previous_statement = self.get_latest_response(input_statement.conversation) if previous_statement: input_statement.in_response_to = previous_statement.text # Make sure the input statement has its search text saved if not input_statement.search_text: _search_text = self.tagger.get_text_index_string(input_statement.text) input_statement.search_text = _search_text if not input_statement.search_in_response_to and input_statement.in_response_to: input_statement.search_in_response_to = self.tagger.get_text_index_string( input_statement.in_response_to ) response = self.generate_response( input_statement, additional_response_selection_parameters ) # If streaming is enabled return the response immediately if self.stream: return response # Update any response data that needs to be changed if persist_values_to_response: for response_key in persist_values_to_response: response_value = persist_values_to_response[response_key] if response_key == 'tags': input_statement.add_tags(*response_value) response.add_tags(*response_value) else: setattr(input_statement, response_key, response_value) setattr(response, response_key, response_value) if not self.read_only: # Save the input statement self.storage.create(**input_statement.serialize()) # Save the response generated for the input self.learn_response(response, previous_statement=input_statement) return response
[docs] def generate_response(self, input_statement, additional_response_selection_parameters=None): """ Return a response based on a given input statement. :param input_statement: The input statement to be processed. """ Statement = self.storage.get_object('statement') results = [] result = None max_confidence = -1 # If a model is provided, use it to process the input statement # instead of the logic adapters if self.model: model_response = self.model.process(input_statement) return model_response for adapter in self.logic_adapters: if adapter.can_process(input_statement): output = adapter.process(input_statement, additional_response_selection_parameters) results.append(output) self.logger.info( '{} selected "{}" as a response with a confidence of {}'.format( adapter.class_name, output.text, output.confidence ) ) if output.confidence > max_confidence: result = output max_confidence = output.confidence else: self.logger.info( 'Not processing the statement using {}'.format(adapter.class_name) ) class ResultOption: def __init__(self, statement, count=1): self.statement = statement self.count = count # If multiple adapters agree on the same statement, # then that statement is more likely to be the correct response if len(results) >= 3: result_options = {} for result_option in results: result_string = result_option.text + ':' + (result_option.in_response_to or '') if result_string in result_options: result_options[result_string].count += 1 if result_options[result_string].statement.confidence < result_option.confidence: result_options[result_string].statement = result_option else: result_options[result_string] = ResultOption( result_option ) most_common = list(result_options.values())[0] for result_option in result_options.values(): if result_option.count > most_common.count: most_common = result_option self.logger.info('Selecting "{}" as the most common response'.format(most_common.statement.text)) if most_common.count > 1: result = most_common.statement response = Statement( text=result.text, in_response_to=input_statement.text, conversation=input_statement.conversation, persona='bot:' + self.name ) response.add_tags(*result.get_tags()) response.confidence = result.confidence return response
[docs] def learn_response(self, statement, previous_statement=None): """ Learn that the statement provided is a valid response. """ if not previous_statement: previous_statement = statement.in_response_to if not previous_statement: previous_statement = self.get_latest_response(statement.conversation) if previous_statement: previous_statement = previous_statement.text previous_statement_text = previous_statement if not isinstance(previous_statement, (str, type(None), )): statement.in_response_to = previous_statement.text elif isinstance(previous_statement, str): statement.in_response_to = previous_statement self.logger.info('Adding "{}" as a response to "{}"'.format( previous_statement_text, statement.text )) if not statement.persona: statement.persona = 'bot:' + self.name # Save the response statement return self.storage.create(**statement.serialize())
[docs] def get_latest_response(self, conversation: str): """ Returns the latest response in a conversation if it exists. Returns None if a matching conversation cannot be found. """ from chatterbot.conversation import Statement as StatementObject conversation_statements = list(self.storage.filter( conversation=conversation, order_by=['id'] )) # Get the most recent statement in the conversation if one exists latest_statement = conversation_statements[-1] if len(conversation_statements) else None return latest_statement
[docs] class ChatBotException(Exception): pass