File: setup.py

#!/usr/bin/env python
"""
ChatterBot setup file.
"""
import os
import sys
import platform
import configparser
from setuptools import setup


if sys.version_info[0] < 3:
    raise Exception(
        'You are tying to install ChatterBot on Python version {}.\n'
        'Please install ChatterBot in Python 3 instead.'.format(
            platform.python_version()
        )
    )

config = configparser.ConfigParser()

current_directory = os.path.dirname(os.path.abspath(__file__))
config_file_path = os.path.join(current_directory, 'setup.cfg')

config.read(config_file_path)

VERSION = config['chatterbot']['version']
AUTHOR = config['chatterbot']['author']
AUTHOR_EMAIL = config['chatterbot']['email']
URL = config['chatterbot']['url']

with open('README.md') as f:
    LONG_DESCRIPTION = f.read()

REQUIREMENTS = []
DEPENDENCIES = []

with open('requirements.txt') as requirements:
    for requirement in requirements.readlines():
        if requirement.startswith('git+git://'):
            DEPENDENCIES.append(requirement)
        else:
            REQUIREMENTS.append(requirement)


setup(
    name='ChatterBot',
    version=VERSION,
    url=URL,
    download_url='{}/tarball/{}'.format(URL, VERSION),
    project_urls={
        'Documentation': 'https://chatterbot.readthedocs.io',
    },
    description='ChatterBot is a machine learning, conversational dialog engine.',
    long_description=LONG_DESCRIPTION,
    long_description_content_type='text/markdown',
    author=AUTHOR,
    author_email=AUTHOR_EMAIL,
    packages=[
        'chatterbot',
        'chatterbot.storage',
        'chatterbot.logic',
        'chatterbot.ext',
        'chatterbot.ext.sqlalchemy_app',
        'chatterbot.ext.django_chatterbot',
        'chatterbot.ext.django_chatterbot.migrations',
    ],
    package_dir={'chatterbot': 'chatterbot'},
    include_package_data=True,
    install_requires=REQUIREMENTS,
    dependency_links=DEPENDENCIES,
    python_requires='>=3.4, <=3.8',
    license='BSD',
    zip_safe=True,
    platforms=['any'],
    keywords=['ChatterBot', 'chatbot', 'chat', 'bot'],
    classifiers=[
        'Development Status :: 4 - Beta',
        'Intended Audience :: Developers',
        'License :: OSI Approved :: BSD License',
        'Environment :: Console',
        'Environment :: Web Environment',
        'Operating System :: OS Independent',
        'Topic :: Software Development :: Libraries :: Python Modules',
        'Topic :: Communications :: Chat',
        'Topic :: Internet',
        'Programming Language :: Python',
        'Programming Language :: Python :: 3',
        'Programming Language :: Python :: 3.4',
        'Programming Language :: Python :: 3.5',
        'Programming Language :: Python :: 3.6',
        'Programming Language :: Python :: 3.7',
        'Programming Language :: Python :: 3.8',
        'Programming Language :: Python :: 3 :: Only',
    ],
    test_suite='tests'
)



File: runtests.py

#!/usr/bin/env python

"""
This is the test runner for the ChatterBot's Django tests.
"""

import os
import sys

import django
from django.conf import settings
from django.test.utils import get_runner

if __name__ == '__main__':
    os.environ['DJANGO_SETTINGS_MODULE'] = 'tests_django.test_settings'
    django.setup()
    TestRunner = get_runner(settings)
    test_runner = TestRunner(
        verbosity=2
    )
    failures = test_runner.run_tests(['tests_django'])
    sys.exit(bool(failures))



File: docs/conf.py

import os
import sys
import configparser
from datetime import datetime
import sphinx_rtd_theme


config = configparser.ConfigParser()

current_directory = os.path.dirname(os.path.abspath(__file__))
parent_directory = os.path.abspath(os.path.join(current_directory, os.pardir))
config_file_path = os.path.join(parent_directory, 'setup.cfg')

config.read(config_file_path)

# Insert the project root dir as the first element in the PYTHONPATH.
# This lets us ensure that the source package is imported, and used to generate the documentation.
sys.path.insert(0, parent_directory)


# Sphinx extension modules
extensions = [
    'sphinx.ext.autodoc',
    'sphinx.ext.autosectionlabel',
    'sphinx.ext.coverage',
    'sphinx.ext.doctest',
    'sphinx.ext.intersphinx',
    'sphinx.ext.mathjax',
    'sphinx.ext.todo',
    'sphinx.ext.viewcode'
]

# Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates']

# The suffix(es) of source filenames.
# You can specify multiple suffix as a list of string:
source_suffix = ['.rst', '.md']

# The encoding of source files
# source_encoding = 'utf-8-sig'

# The master toctree document
master_doc = 'index'

# General information about the project
project = 'ChatterBot'
author = config['chatterbot']['author']
copyright = '{}, {}'.format(
    datetime.now().year,
    author
)

# The full version, including alpha/beta/rc tags
release = config['chatterbot']['version']

# The short X.Y version
version = config['chatterbot']['version'].rsplit('.', 1)[0]

language = 'en'

# List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files.
# This patterns also effect to html_static_path and html_extra_path
exclude_patterns = []

# If true, '()' will be appended to :func: etc. cross-reference text
# add_function_parentheses = True

# If true, the current module name will be prepended to all description
# unit titles (such as .. function::)
# add_module_names = True

# If true, sectionauthor and moduleauthor directives will be shown in the
# output. They are ignored by default.
# show_authors = False

# The name of the Pygments (syntax highlighting) style to use
pygments_style = 'sphinx'

# -- Options for HTML output ----------------------------------------------

html_theme = 'sphinx_rtd_theme'

html_theme_path = [sphinx_rtd_theme.get_html_theme_path()]
# Theme options are theme-specific and customize the look and feel of a theme
# further. For a list of options available for each theme, see the
# documentation.
html_theme_options = {
    'logo_only': True
}

html_show_sourcelink = False

# A shorter title for the navigation bar. Default is the same as html_title.
# html_short_title = None

# The name of an image file (relative to this directory) to place at the top
# of the sidebar.
html_logo = '../graphics/banner.png'

# The name of an image file (relative to this directory) to use as a favicon of
# the docs.  This file should be a Windows icon file (.ico) being 16x16 or 32x32
# pixels large.
html_favicon = '_static/favicon.ico'

# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css".
html_static_path = ['_static']

# Add any extra paths that contain custom files (such as robots.txt or
# .htaccess) here, relative to this directory. These files are copied
# directly to the root of the documentation.
# html_extra_path = []

# If not None, a 'Last updated on:' timestamp is inserted at every page
# bottom, using the given strftime format.
# The empty string is equivalent to '%b %d, %Y'.
# html_last_updated_fmt = None

# Custom sidebar templates, maps document names to template names.
# html_sidebars = {}

# Additional templates that should be rendered to pages, maps page names to
# template names.
# html_additional_pages = {}

# If false, no module index is generated.
# html_domain_indices = True

# If false, no index is generated.
# html_use_index = True

# Split the index into individual pages for each letter.
html_split_index = True

# If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
html_show_sphinx = False

# Language to be used for generating the HTML full-text search index.
# Sphinx supports the following languages:
#   'da', 'de', 'en', 'es', 'fi', 'fr', 'hu', 'it', 'ja'
#   'nl', 'no', 'pt', 'ro', 'ru', 'sv', 'tr', 'zh'
html_search_language = 'en'

# Output file base name for HTML help builder
htmlhelp_basename = 'ChatterBotdoc'

# Read the docs theme modifications

html_context = {
    'extra_css_files': [
        '_static/style.css'
    ]
}

# Grouping the document tree into LaTeX files. List of tuples
# (source start file, target name, title,
#  author, documentclass [howto, manual, or own class])
latex_documents = [
    (master_doc, 'ChatterBot.tex', u'ChatterBot Documentation',
     u'Gunther Cox', 'manual'),
]

# -- Options for manual page output ---------------------------------------

# One entry per manual page. List of tuples
# (source start file, name, description, authors, manual section)
man_pages = [
    (master_doc, 'chatterbot', u'ChatterBot Documentation',
     [author], 1)
]

# -- Options for Texinfo output -------------------------------------------

# Grouping the document tree into Texinfo files. List of tuples
# (source start file, target name, title, author,
#  dir menu entry, description, category)
texinfo_documents = [
    (master_doc, 'ChatterBot', u'ChatterBot Documentation',
     author, 'ChatterBot', 'One line description of project.',
     'Miscellaneous'),
]

# -- Options for Epub output ----------------------------------------------

# Bibliographic Dublin Core info
epub_title = project
epub_author = author
epub_publisher = author
epub_copyright = copyright

# A list of files that should not be packed into the epub file
epub_exclude_files = ['search.html']

# Configuration for intersphinx
intersphinx_mapping = {
    'python': ('https://docs.python.org/3', None)
}



File: chatterbot/response_selection.py

"""
Response selection methods determines which response should be used in
the event that multiple responses are generated within a logic adapter.
"""
import logging


def get_most_frequent_response(input_statement, response_list, storage=None):
    """
    :param input_statement: A statement, that closely matches an input to the chat bot.
    :type input_statement: Statement

    :param response_list: A list of statement options to choose a response from.
    :type response_list: list

    :param storage: An instance of a storage adapter to allow the response selection
                    method to access other statements if needed.
    :type storage: StorageAdapter

    :return: The response statement with the greatest number of occurrences.
    :rtype: Statement
    """
    matching_response = None
    occurrence_count = -1

    logger = logging.getLogger(__name__)
    logger.info('Selecting response with greatest number of occurrences.')

    for statement in response_list:
        count = len(list(storage.filter(
            text=statement.text,
            in_response_to=input_statement.text)
        ))

        # Keep the more common statement
        if count >= occurrence_count:
            matching_response = statement
            occurrence_count = count

    # Choose the most commonly occuring matching response
    return matching_response


def get_first_response(input_statement, response_list, storage=None):
    """
    :param input_statement: A statement, that closely matches an input to the chat bot.
    :type input_statement: Statement

    :param response_list: A list of statement options to choose a response from.
    :type response_list: list

    :param storage: An instance of a storage adapter to allow the response selection
                    method to access other statements if needed.
    :type storage: StorageAdapter

    :return: Return the first statement in the response list.
    :rtype: Statement
    """
    logger = logging.getLogger(__name__)
    logger.info('Selecting first response from list of {} options.'.format(
        len(response_list)
    ))
    return response_list[0]


def get_random_response(input_statement, response_list, storage=None):
    """
    :param input_statement: A statement, that closely matches an input to the chat bot.
    :type input_statement: Statement

    :param response_list: A list of statement options to choose a response from.
    :type response_list: list

    :param storage: An instance of a storage adapter to allow the response selection
                    method to access other statements if needed.
    :type storage: StorageAdapter

    :return: Choose a random response from the selection.
    :rtype: Statement
    """
    from random import choice
    logger = logging.getLogger(__name__)
    logger.info('Selecting a response from list of {} options.'.format(
        len(response_list)
    ))
    return choice(response_list)



File: chatterbot/parsing.py

import re
from datetime import timedelta, datetime
import calendar

# Variations of dates that the parser can capture
year_variations = ['year', 'years', 'yrs']
day_variations = ['days', 'day']
minute_variations = ['minute', 'minutes', 'mins']
hour_variations = ['hrs', 'hours', 'hour']
week_variations = ['weeks', 'week', 'wks']
month_variations = ['month', 'months']

# Variables used for RegEx Matching
day_names = 'monday|tuesday|wednesday|thursday|friday|saturday|sunday'
month_names_long = (
    'january|february|march|april|may|june|july|august|september|october|november|december'
)
month_names = month_names_long + '|jan|feb|mar|apr|may|jun|jul|aug|sep|oct|nov|dec'
day_nearest_names = 'today|yesterday|tomorrow|tonight|tonite'
numbers = (
    r'(^a(?=\s)|one|two|three|four|five|six|seven|eight|nine|ten|'
    r'eleven|twelve|thirteen|fourteen|fifteen|sixteen|seventeen|'
    r'eighteen|nineteen|twenty|thirty|forty|fifty|sixty|seventy|'
    r'eighty|ninety|hundred|thousand)'
)
re_dmy = '(' + '|'.join(day_variations + minute_variations + year_variations + week_variations + month_variations) + ')'
re_duration = r'(before|after|earlier|later|ago|from\snow)'
re_year = r'(19|20)\d{2}|^(19|20)\d{2}'
re_timeframe = r'this|coming|next|following|previous|last|end\sof\sthe'
re_ordinal = r'st|nd|rd|th|first|second|third|fourth|fourth|' + re_timeframe
re_time = r'(?P<hour>\d{1,2})(?=\s?(\:\d|(a|p)m))(\:(?P<minute>\d{1,2}))?(\s?(?P<convention>(am|pm)))?'
re_separator = r'of|at|on'

NUMBERS = {
    'zero': 0,
    'one': 1,
    'two': 2,
    'three': 3,
    'four': 4,
    'five': 5,
    'six': 6,
    'seven': 7,
    'eight': 8,
    'nine': 9,
    'ten': 10,
    'eleven': 11,
    'twelve': 12,
    'thirteen': 13,
    'fourteen': 14,
    'fifteen': 15,
    'sixteen': 16,
    'seventeen': 17,
    'eighteen': 18,
    'nineteen': 19,
    'twenty': 20,
    'thirty': 30,
    'forty': 40,
    'fifty': 50,
    'sixty': 60,
    'seventy': 70,
    'eighty': 80,
    'ninety': 90,
    'hundred': 100,
    'thousand': 1000,
    'million': 1000000,
    'billion': 1000000000,
    'trillion': 1000000000000,
}


# Mapping of Month name and Value
HASHMONTHS = {
    'january': 1,
    'jan': 1,
    'february': 2,
    'feb': 2,
    'march': 3,
    'mar': 3,
    'april': 4,
    'apr': 4,
    'may': 5,
    'june': 6,
    'jun': 6,
    'july': 7,
    'jul': 7,
    'august': 8,
    'aug': 8,
    'september': 9,
    'sep': 9,
    'october': 10,
    'oct': 10,
    'november': 11,
    'nov': 11,
    'december': 12,
    'dec': 12
}

# Days to number mapping
HASHWEEKDAYS = {
    'monday': 0,
    'mon': 0,
    'tuesday': 1,
    'tue': 1,
    'wednesday': 2,
    'wed': 2,
    'thursday': 3,
    'thu': 3,
    'friday': 4,
    'fri': 4,
    'saturday': 5,
    'sat': 5,
    'sunday': 6,
    'sun': 6
}

# Ordinal to number
HASHORDINALS = {
    'zeroth': 0,
    'first': 1,
    'second': 2,
    'third': 3,
    'fourth': 4,
    'forth': 4,
    'fifth': 5,
    'sixth': 6,
    'seventh': 7,
    'eighth': 8,
    'ninth': 9,
    'tenth': 10,
    'eleventh': 11,
    'twelfth': 12,
    'thirteenth': 13,
    'fourteenth': 14,
    'fifteenth': 15,
    'sixteenth': 16,
    'seventeenth': 17,
    'eighteenth': 18,
    'nineteenth': 19,
    'twentieth': 20,
    'last': -1
}

# A list tuple of regular expressions / parser fn to match
# Start with the widest match and narrow it down because the order of the match in this list matters
regex = [
    (
        re.compile(
            r'''
            (
                ((?P<dow>%s)[,\s]\s*)? #Matches Monday, 12 Jan 2012, 12 Jan 2012 etc
                (?P<day>\d{1,2}) # Matches a digit
                (%s)?
                [-\s] # One or more space
                (?P<month>%s) # Matches any month name
                [-\s] # Space
                (?P<year>%s) # Year
                ((\s|,\s|\s(%s))?\s*(%s))?
            )
            ''' % (day_names, re_ordinal, month_names, re_year, re_separator, re_time),
            (re.VERBOSE | re.IGNORECASE)
        ),
        lambda m, base_date: datetime(
            int(m.group('year') if m.group('year') else base_date.year),
            HASHMONTHS[m.group('month').strip().lower()],
            int(m.group('day') if m.group('day') else 1),
        ) + timedelta(**convert_time_to_hour_minute(
            m.group('hour'),
            m.group('minute'),
            m.group('convention')
        ))
    ),
    (
        re.compile(
            r'''
            (
                ((?P<dow>%s)[,\s][-\s]*)? #Matches Monday, Jan 12 2012, Jan 12 2012 etc
                (?P<month>%s) # Matches any month name
                [-\s] # Space
                ((?P<day>\d{1,2})) # Matches a digit
                (%s)?
                ([-\s](?P<year>%s))? # Year
                ((\s|,\s|\s(%s))?\s*(%s))?
            )
            ''' % (day_names, month_names, re_ordinal, re_year, re_separator, re_time),
            (re.VERBOSE | re.IGNORECASE)
        ),
        lambda m, base_date: datetime(
            int(m.group('year') if m.group('year') else base_date.year),
            HASHMONTHS[m.group('month').strip().lower()],
            int(m.group('day') if m.group('day') else 1)
        ) + timedelta(**convert_time_to_hour_minute(
            m.group('hour'),
            m.group('minute'),
            m.group('convention')
        ))
    ),
    (
        re.compile(
            r'''
            (
                (?P<month>%s) # Matches any month name
                [-\s] # One or more space
                (?P<day>\d{1,2}) # Matches a digit
                (%s)?
                [-\s]\s*?
                (?P<year>%s) # Year
                ((\s|,\s|\s(%s))?\s*(%s))?
            )
            ''' % (month_names, re_ordinal, re_year, re_separator, re_time),
            (re.VERBOSE | re.IGNORECASE)
        ),
        lambda m, base_date: datetime(
            int(m.group('year') if m.group('year') else base_date.year),
            HASHMONTHS[m.group('month').strip().lower()],
            int(m.group('day') if m.group('day') else 1),
        ) + timedelta(**convert_time_to_hour_minute(
            m.group('hour'),
            m.group('minute'),
            m.group('convention')
        ))
    ),
    (
        re.compile(
            r'''
            (
                ((?P<number>\d+|(%s[-\s]?)+)\s)? # Matches any number or string 25 or twenty five
                (?P<unit>%s)s?\s # Matches days, months, years, weeks, minutes
                (?P<duration>%s) # before, after, earlier, later, ago, from now
                (\s*(?P<base_time>(%s)))?
                ((\s|,\s|\s(%s))?\s*(%s))?
            )
            ''' % (numbers, re_dmy, re_duration, day_nearest_names, re_separator, re_time),
            (re.VERBOSE | re.IGNORECASE)
        ),
        lambda m, base_date: date_from_duration(
            base_date,
            m.group('number'),
            m.group('unit').lower(),
            m.group('duration').lower(),
            m.group('base_time')
        ) + timedelta(**convert_time_to_hour_minute(
            m.group('hour'),
            m.group('minute'),
            m.group('convention')
        ))
    ),
    (
        re.compile(
            r'''
            (
                (?P<ordinal>%s) # First quarter of 2014
                \s+
                quarter\sof
                \s+
                (?P<year>%s)
            )
            ''' % (re_ordinal, re_year),
            (re.VERBOSE | re.IGNORECASE)
        ),
        lambda m, base_date: date_from_quarter(
            base_date,
            HASHORDINALS[m.group('ordinal').lower()],
            int(m.group('year') if m.group('year') else base_date.year)
        )
    ),
    (
        re.compile(
            r'''
            (
                (?P<ordinal_value>\d+)
                (?P<ordinal>%s) # 1st January 2012
                ((\s|,\s|\s(%s))?\s*)?
                (?P<month>%s)
                ([,\s]\s*(?P<year>%s))?
            )
            ''' % (re_ordinal, re_separator, month_names, re_year),
            (re.VERBOSE | re.IGNORECASE)
        ),
        lambda m, base_date: datetime(
            int(m.group('year') if m.group('year') else base_date.year),
            int(HASHMONTHS[m.group('month').lower()] if m.group('month') else 1),
            int(m.group('ordinal_value') if m.group('ordinal_value') else 1),
        )
    ),
    (
        re.compile(
            r'''
            (
                (?P<month>%s)
                \s+
                (?P<ordinal_value>\d+)
                (?P<ordinal>%s) # January 1st 2012
                ([,\s]\s*(?P<year>%s))?
            )
            ''' % (month_names, re_ordinal, re_year),
            (re.VERBOSE | re.IGNORECASE)
        ),
        lambda m, base_date: datetime(
            int(m.group('year') if m.group('year') else base_date.year),
            int(HASHMONTHS[m.group('month').lower()] if m.group('month') else 1),
            int(m.group('ordinal_value') if m.group('ordinal_value') else 1),
        )
    ),
    (
        re.compile(
            r'''
            (?P<time>%s) # this, next, following, previous, last
            \s+
            ((?P<number>\d+|(%s[-\s]?)+)\s)?
            (?P<dmy>%s) # year, day, week, month, night, minute, min
            ((\s|,\s|\s(%s))?\s*(%s))?
            ''' % (re_timeframe, numbers, re_dmy, re_separator, re_time),
            (re.VERBOSE | re.IGNORECASE),
        ),
        lambda m, base_date: date_from_relative_week_year(
            base_date,
            m.group('time').lower(),
            m.group('dmy').lower(),
            m.group('number')
        ) + timedelta(**convert_time_to_hour_minute(
            m.group('hour'),
            m.group('minute'),
            m.group('convention')
        ))
    ),
    (
        re.compile(
            r'''
            (?P<time>%s) # this, next, following, previous, last
            \s+
            (?P<dow>%s) # mon - fri
            ((\s|,\s|\s(%s))?\s*(%s))?
            ''' % (re_timeframe, day_names, re_separator, re_time),
            (re.VERBOSE | re.IGNORECASE),
        ),
        lambda m, base_date: date_from_relative_day(
            base_date,
            m.group('time').lower(),
            m.group('dow')
        ) + timedelta(**convert_time_to_hour_minute(
            m.group('hour'),
            m.group('minute'),
            m.group('convention')
        ))
    ),
    (
        re.compile(
            r'''
            (
                (?P<day>\d{1,2}) # Day, Month
                (%s)
                [-\s] # One or more space
                (?P<month>%s)
            )
            ''' % (re_ordinal, month_names),
            (re.VERBOSE | re.IGNORECASE)
        ),
        lambda m, base_date: datetime(
            base_date.year,
            HASHMONTHS[m.group('month').strip().lower()],
            int(m.group('day') if m.group('day') else 1)
        )
    ),
    (
        re.compile(
            r'''
            (
                (?P<month>%s) # Month, day
                [-\s] # One or more space
                ((?P<day>\d{1,2})\b) # Matches a digit January 12
                (%s)?
            )
            ''' % (month_names, re_ordinal),
            (re.VERBOSE | re.IGNORECASE)
        ),
        lambda m, base_date: datetime(
            base_date.year,
            HASHMONTHS[m.group('month').strip().lower()],
            int(m.group('day') if m.group('day') else 1)
        )
    ),
    (
        re.compile(
            r'''
            (
                (?P<month>%s) # Month, year
                [-\s] # One or more space
                ((?P<year>\d{1,4})\b) # Matches a digit January 12
            )
            ''' % (month_names),
            (re.VERBOSE | re.IGNORECASE)
        ),
        lambda m, base_date: datetime(
            int(m.group('year')),
            HASHMONTHS[m.group('month').strip().lower()],
            1
        )
    ),
    (
        re.compile(
            r'''
            (
                (?P<month>\d{1,2}) # MM/DD or MM/DD/YYYY
                /
                ((?P<day>\d{1,2}))
                (/(?P<year>%s))?
            )
            ''' % (re_year),
            (re.VERBOSE | re.IGNORECASE)
        ),
        lambda m, base_date: datetime(
            int(m.group('year') if m.group('year') else base_date.year),
            int(m.group('month').strip()),
            int(m.group('day'))
        )
    ),
    (
        re.compile(
            r'''
            (?P<adverb>%s) # today, yesterday, tomorrow, tonight
            ((\s|,\s|\s(%s))?\s*(%s))?
            ''' % (day_nearest_names, re_separator, re_time),
            (re.VERBOSE | re.IGNORECASE)
        ),
        lambda m, base_date: date_from_adverb(
            base_date,
            m.group('adverb')
        ) + timedelta(**convert_time_to_hour_minute(
            m.group('hour'),
            m.group('minute'),
            m.group('convention')
        ))
    ),
    (
        re.compile(
            r'''
            (?P<named_day>%s) # Mon - Sun
            ''' % (day_names),
            (re.VERBOSE | re.IGNORECASE)
        ),
        lambda m, base_date: this_week_day(
            base_date,
            HASHWEEKDAYS[m.group('named_day').lower()]
        )
    ),
    (
        re.compile(
            r'''
            (?P<year>%s) # Year
            ''' % (re_year),
            (re.VERBOSE | re.IGNORECASE)
        ),
        lambda m, base_date: datetime(int(m.group('year')), 1, 1)
    ),
    (
        re.compile(
            r'''
            (?P<month>%s) # Month
            ''' % (month_names_long),
            (re.VERBOSE | re.IGNORECASE)
        ),
        lambda m, base_date: datetime(
            base_date.year,
            HASHMONTHS[m.group('month').lower()],
            1
        )
    ),
    (
        re.compile(
            r'''
            (%s) # Matches time 12:00 am or 12:00 pm
            ''' % (re_time),
            (re.VERBOSE | re.IGNORECASE),
        ),
        lambda m, base_date: datetime(
            base_date.year,
            base_date.month,
            base_date.day
        ) + timedelta(**convert_time_to_hour_minute(
            m.group('hour'),
            m.group('minute'),
            m.group('convention')
        ))
    ),
    (
        re.compile(
            r'''
            (
                (?P<hour>\d+) # Matches 12 hours, 2 hrs
                \s+
                (%s)
            )
            ''' % ('|'.join(hour_variations)),
            (re.VERBOSE | re.IGNORECASE),
        ),
        lambda m, base_date: datetime(
            base_date.year,
            base_date.month,
            base_date.day,
            int(m.group('hour'))
        )
    )
]


def convert_string_to_number(value):
    """
    Convert strings to numbers
    """
    if value is None:
        return 1
    if isinstance(value, int):
        return value
    if value.isdigit():
        return int(value)
    num_list = map(lambda s: NUMBERS[s], re.findall(numbers + '+', value.lower()))
    return sum(num_list)


def convert_time_to_hour_minute(hour, minute, convention):
    """
    Convert time to hour, minute
    """
    if hour is None:
        hour = 0
    if minute is None:
        minute = 0
    if convention is None:
        convention = 'am'

    hour = int(hour)
    minute = int(minute)

    if convention.lower() == 'pm':
        hour += 12

    return {'hours': hour, 'minutes': minute}


def date_from_quarter(base_date, ordinal, year):
    """
    Extract date from quarter of a year
    """
    interval = 3
    month_start = interval * (ordinal - 1)
    if month_start < 0:
        month_start = 9
    month_end = month_start + interval
    if month_start == 0:
        month_start = 1
    return [
        datetime(year, month_start, 1),
        datetime(year, month_end, calendar.monthrange(year, month_end)[1])
    ]


def date_from_relative_day(base_date, time, dow):
    """
    Converts relative day to time
    Ex: this tuesday, last tuesday
    """
    # Reset date to start of the day
    base_date = datetime(base_date.year, base_date.month, base_date.day)
    time = time.lower()
    dow = dow.lower()
    if time == 'this' or time == 'coming':
        # Else day of week
        num = HASHWEEKDAYS[dow]
        return this_week_day(base_date, num)
    elif time == 'last' or time == 'previous':
        # Else day of week
        num = HASHWEEKDAYS[dow]
        return previous_week_day(base_date, num)
    elif time == 'next' or time == 'following':
        # Else day of week
        num = HASHWEEKDAYS[dow]
        return next_week_day(base_date, num)


def date_from_relative_week_year(base_date, time, dow, ordinal=1):
    """
    Converts relative day to time
    Eg. this tuesday, last tuesday
    """
    # If there is an ordinal (next 3 weeks) => return a start and end range
    # Reset date to start of the day
    relative_date = datetime(base_date.year, base_date.month, base_date.day)
    ord = convert_string_to_number(ordinal)
    if dow in year_variations:
        if time == 'this' or time == 'coming':
            return datetime(relative_date.year, 1, 1)
        elif time == 'last' or time == 'previous':
            return datetime(relative_date.year - 1, relative_date.month, 1)
        elif time == 'next' or time == 'following':
            return relative_date + timedelta(ord * 365)
        elif time == 'end of the':
            return datetime(relative_date.year, 12, 31)
    elif dow in month_variations:
        if time == 'this':
            return datetime(relative_date.year, relative_date.month, relative_date.day)
        elif time == 'last' or time == 'previous':
            return datetime(relative_date.year, relative_date.month - 1, relative_date.day)
        elif time == 'next' or time == 'following':
            if relative_date.month + ord >= 12:
                month = relative_date.month - 1 + ord
                year = relative_date.year + month // 12
                month = month % 12 + 1
                day = min(relative_date.day, calendar.monthrange(year, month)[1])
                return datetime(year, month, day)
            else:
                return datetime(relative_date.year, relative_date.month + ord, relative_date.day)
        elif time == 'end of the':
            return datetime(
                relative_date.year,
                relative_date.month,
                calendar.monthrange(relative_date.year, relative_date.month)[1]
            )
    elif dow in week_variations:
        if time == 'this':
            return relative_date - timedelta(days=relative_date.weekday())
        elif time == 'last' or time == 'previous':
            return relative_date - timedelta(weeks=1)
        elif time == 'next' or time == 'following':
            return relative_date + timedelta(weeks=ord)
        elif time == 'end of the':
            day_of_week = base_date.weekday()
            return day_of_week + timedelta(days=6 - relative_date.weekday())
    elif dow in day_variations:
        if time == 'this':
            return relative_date
        elif time == 'last' or time == 'previous':
            return relative_date - timedelta(days=1)
        elif time == 'next' or time == 'following':
            return relative_date + timedelta(days=ord)
        elif time == 'end of the':
            return datetime(relative_date.year, relative_date.month, relative_date.day, 23, 59, 59)


def date_from_adverb(base_date, name):
    """
    Convert Day adverbs to dates
    Tomorrow => Date
    Today => Date
    """
    # Reset date to start of the day
    adverb_date = datetime(base_date.year, base_date.month, base_date.day)
    if name == 'today' or name == 'tonite' or name == 'tonight':
        return adverb_date.today().replace(hour=0, minute=0, second=0, microsecond=0)
    elif name == 'yesterday':
        return adverb_date - timedelta(days=1)
    elif name == 'tomorrow' or name == 'tom':
        return adverb_date + timedelta(days=1)


def date_from_duration(base_date, number_as_string, unit, duration, base_time=None):
    """
    Find dates from duration
    Eg: 20 days from now
    Currently does not support strings like "20 days from last monday".
    """
    # Check if query is `2 days before yesterday` or `day before yesterday`
    if base_time is not None:
        base_date = date_from_adverb(base_date, base_time)
    num = convert_string_to_number(number_as_string)
    if unit in day_variations:
        args = {'days': num}
    elif unit in minute_variations:
        args = {'minutes': num}
    elif unit in week_variations:
        args = {'weeks': num}
    elif unit in month_variations:
        args = {'days': 365 * num / 12}
    elif unit in year_variations:
        args = {'years': num}
    if duration == 'ago' or duration == 'before' or duration == 'earlier':
        if 'years' in args:
            return datetime(base_date.year - args['years'], base_date.month, base_date.day)
        return base_date - timedelta(**args)
    elif duration == 'after' or duration == 'later' or duration == 'from now':
        if 'years' in args:
            return datetime(base_date.year + args['years'], base_date.month, base_date.day)
        return base_date + timedelta(**args)


def this_week_day(base_date, weekday):
    """
    Finds coming weekday
    """
    day_of_week = base_date.weekday()
    # If today is Tuesday and the query is `this monday`
    # We should output the next_week monday
    if day_of_week > weekday:
        return next_week_day(base_date, weekday)
    start_of_this_week = base_date - timedelta(days=day_of_week + 1)
    day = start_of_this_week + timedelta(days=1)
    while day.weekday() != weekday:
        day = day + timedelta(days=1)
    return day


def previous_week_day(base_date, weekday):
    """
    Finds previous weekday
    """
    day = base_date - timedelta(days=1)
    while day.weekday() != weekday:
        day = day - timedelta(days=1)
    return day


def next_week_day(base_date, weekday):
    """
    Finds next weekday
    """
    day_of_week = base_date.weekday()
    end_of_this_week = base_date + timedelta(days=6 - day_of_week)
    day = end_of_this_week + timedelta(days=1)
    while day.weekday() != weekday:
        day = day + timedelta(days=1)
    return day


def datetime_parsing(text, base_date=datetime.now()):
    """
    Extract datetime objects from a string of text.
    """
    matches = []
    found_array = []

    # Find the position in the string
    for expression, function in regex:
        for match in expression.finditer(text):
            matches.append((match.group(), function(match, base_date), match.span()))

    # Wrap the matched text with TAG element to prevent nested selections
    for match, value, spans in matches:
        subn = re.subn(
            '(?!<TAG[^>]*?>)' + match + '(?![^<]*?</TAG>)', '<TAG>' + match + '</TAG>', text
        )
        text = subn[0]
        is_substituted = subn[1]
        if is_substituted != 0:
            found_array.append((match, value, spans))

    # To preserve order of the match, sort based on the start position
    return sorted(found_array, key=lambda match: match and match[2][0])



File: chatterbot/comparisons.py

"""
This module contains various text-comparison algorithms
designed to compare one statement to another.
"""
from chatterbot.exceptions import OptionalDependencyImportError
from difflib import SequenceMatcher


class Comparator:

    def __init__(self, language):

        self.language = language

    def __call__(self, statement_a, statement_b):
        return self.compare(statement_a, statement_b)

    def compare(self, statement_a, statement_b):
        return 0


class LevenshteinDistance(Comparator):
    """
    Compare two statements based on the Levenshtein distance
    of each statement's text.

    For example, there is a 65% similarity between the statements
    "where is the post office?" and "looking for the post office"
    based on the Levenshtein distance algorithm.
    """

    def compare(self, statement_a, statement_b):
        """
        Compare the two input statements.

        :return: The percent of similarity between the text of the statements.
        :rtype: float
        """

        # Return 0 if either statement has a falsy text value
        if not statement_a.text or not statement_b.text:
            return 0

        # Get the lowercase version of both strings
        statement_a_text = str(statement_a.text.lower())
        statement_b_text = str(statement_b.text.lower())

        similarity = SequenceMatcher(
            None,
            statement_a_text,
            statement_b_text
        )

        # Calculate a decimal percent of the similarity
        percent = round(similarity.ratio(), 2)

        return percent


class SpacySimilarity(Comparator):
    """
    Calculate the similarity of two statements using Spacy models.
    """

    def __init__(self, language):
        super().__init__(language)
        try:
            import spacy
        except ImportError:
            message = (
                'Unable to import "spacy".\n'
                'Please install "spacy" before using the SpacySimilarity comparator:\n'
                'pip3 install "spacy>=2.1,<2.2"'
            )
            raise OptionalDependencyImportError(message)

        self.nlp = spacy.load(self.language.ISO_639_1)

    def compare(self, statement_a, statement_b):
        """
        Compare the two input statements.

        :return: The percent of similarity between the closest synset distance.
        :rtype: float
        """
        document_a = self.nlp(statement_a.text)
        document_b = self.nlp(statement_b.text)

        return document_a.similarity(document_b)


class JaccardSimilarity(Comparator):
    """
    Calculates the similarity of two statements based on the Jaccard index.

    The Jaccard index is composed of a numerator and denominator.
    In the numerator, we count the number of items that are shared between the sets.
    In the denominator, we count the total number of items across both sets.
    Let's say we define sentences to be equivalent if 50% or more of their tokens are equivalent.
    Here are two sample sentences:

        The young cat is hungry.
        The cat is very hungry.

    When we parse these sentences to remove stopwords, we end up with the following two sets:

        {young, cat, hungry}
        {cat, very, hungry}

    In our example above, our intersection is {cat, hungry}, which has count of two.
    The union of the sets is {young, cat, very, hungry}, which has a count of four.
    Therefore, our `Jaccard similarity index`_ is two divided by four, or 50%.
    Given our similarity threshold above, we would consider this to be a match.

    .. _`Jaccard similarity index`: https://en.wikipedia.org/wiki/Jaccard_index
    """

    def __init__(self, language):
        super().__init__(language)
        try:
            import spacy
        except ImportError:
            message = (
                'Unable to import "spacy".\n'
                'Please install "spacy" before using the JaccardSimilarity comparator:\n'
                'pip3 install "spacy>=2.1,<2.2"'
            )
            raise OptionalDependencyImportError(message)

        self.nlp = spacy.load(self.language.ISO_639_1)

    def compare(self, statement_a, statement_b):
        """
        Return the calculated similarity of two
        statements based on the Jaccard index.
        """
        # Make both strings lowercase
        document_a = self.nlp(statement_a.text.lower())
        document_b = self.nlp(statement_b.text.lower())

        statement_a_lemmas = set([
            token.lemma_ for token in document_a if not token.is_stop
        ])
        statement_b_lemmas = set([
            token.lemma_ for token in document_b if not token.is_stop
        ])

        # Calculate Jaccard similarity
        numerator = len(statement_a_lemmas.intersection(statement_b_lemmas))
        denominator = float(len(statement_a_lemmas.union(statement_b_lemmas)))
        ratio = numerator / denominator

        return ratio



File: chatterbot/conversation.py

from pytz import UTC
from datetime import datetime
from dateutil import parser as date_parser


class StatementMixin(object):
    """
    This class has shared methods used to
    normalize different statement models.
    """

    statement_field_names = [
        'id',
        'text',
        'search_text',
        'conversation',
        'persona',
        'tags',
        'in_response_to',
        'search_in_response_to',
        'created_at',
    ]

    extra_statement_field_names = []

    def get_statement_field_names(self):
        """
        Return the list of field names for the statement.
        """
        return self.statement_field_names + self.extra_statement_field_names

    def get_tags(self):
        """
        Return the list of tags for this statement.
        """
        return self.tags

    def add_tags(self, *tags):
        """
        Add a list of strings to the statement as tags.
        """
        self.tags.extend(tags)

    def serialize(self):
        """
        :returns: A dictionary representation of the statement object.
        :rtype: dict
        """
        data = {}

        for field_name in self.get_statement_field_names():
            format_method = getattr(self, 'get_{}'.format(
                field_name
            ), None)

            if format_method:
                data[field_name] = format_method()
            else:
                data[field_name] = getattr(self, field_name)

        return data


class Statement(StatementMixin):
    """
    A statement represents a single spoken entity, sentence or
    phrase that someone can say.
    """

    __slots__ = (
        'id',
        'text',
        'search_text',
        'conversation',
        'persona',
        'tags',
        'in_response_to',
        'search_in_response_to',
        'created_at',
        'confidence',
        'storage',
    )

    def __init__(self, text, in_response_to=None, **kwargs):

        self.id = kwargs.get('id')
        self.text = str(text)
        self.search_text = kwargs.get('search_text', '')
        self.conversation = kwargs.get('conversation', '')
        self.persona = kwargs.get('persona', '')
        self.tags = kwargs.pop('tags', [])
        self.in_response_to = in_response_to
        self.search_in_response_to = kwargs.get('search_in_response_to', '')
        self.created_at = kwargs.get('created_at', datetime.now())

        if not isinstance(self.created_at, datetime):
            self.created_at = date_parser.parse(self.created_at)

        # Set timezone to UTC if no timezone was provided
        if not self.created_at.tzinfo:
            self.created_at = self.created_at.replace(tzinfo=UTC)

        # This is the confidence with which the chat bot believes
        # this is an accurate response. This value is set when the
        # statement is returned by the chat bot.
        self.confidence = 0

        self.storage = None

    def __str__(self):
        return self.text

    def __repr__(self):
        return '<Statement text:%s>' % (self.text)

    def save(self):
        """
        Save the statement in the database.
        """
        self.storage.update(self)



File: chatterbot/constants.py

"""
ChatterBot constants
"""

'''
The maximum length of characters that the text of a statement can contain.
The number 255 is used because that is the maximum length of a char field
in most databases. This value should be enforced on a per-model basis by
the data model for each storage adapter.
'''
STATEMENT_TEXT_MAX_LENGTH = 255

'''
The maximum length of characters that the text label of a conversation can contain.
The number 32 was chosen because that is the length of the string representation
of a UUID4 with no hyphens.
'''
CONVERSATION_LABEL_MAX_LENGTH = 32

'''
The maximum length of text that can be stored in the persona field of the statement model.
'''
PERSONA_MAX_LENGTH = 50

# The maximum length of characters that the name of a tag can contain
TAG_NAME_MAX_LENGTH = 50

DEFAULT_DJANGO_APP_NAME = 'django_chatterbot'



File: chatterbot/__init__.py

"""
ChatterBot is a machine learning, conversational dialog engine.
"""
from .chatterbot import ChatBot


__all__ = (
    'ChatBot',
)



File: chatterbot/chatterbot.py

import logging
from chatterbot.storage import StorageAdapter
from chatterbot.logic import LogicAdapter
from chatterbot.search import TextSearch, IndexedTextSearch
from chatterbot import utils


class ChatBot(object):
    """
    A conversational dialog chat bot.
    """

    def __init__(self, name, **kwargs):
        self.name = 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)

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

        self.logger = kwargs.get('logger', logging.getLogger(__name__))

        # Allow the bot to save input it receives so that it can learn
        self.read_only = kwargs.get('read_only', False)

    def get_response(self, statement=None, **kwargs):
        """
        Return the bot's response based on the input.

        :param statement: An statement object or string.
        :returns: A response to the input.
        :rtype: Statement

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

        # Make sure the input statement has its search text saved

        if not input_statement.search_text:
            input_statement.search_text = self.storage.tagger.get_text_index_string(input_statement.text)

        if not input_statement.search_in_response_to and input_statement.in_response_to:
            input_statement.search_in_response_to = self.storage.tagger.get_text_index_string(input_statement.in_response_to)

        response = self.generate_response(input_statement, additional_response_selection_parameters)

        # 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:
            self.learn_response(input_statement)

            # Save the response generated for the input
            self.storage.create(**response.serialize())

        return response

    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

        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

            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.confidence = result.confidence

        return response

    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(
            statement.text,
            previous_statement_text
        ))

        # Save the input statement
        return self.storage.create(**statement.serialize())

    def get_latest_response(self, conversation):
        """
        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 conversation_statements else None

        if latest_statement:
            if latest_statement.in_response_to:

                response_statements = list(self.storage.filter(
                    conversation=conversation,
                    text=latest_statement.in_response_to,
                    order_by=['id']
                ))

                if response_statements:
                    return response_statements[-1]
                else:
                    return StatementObject(
                        text=latest_statement.in_response_to,
                        conversation=conversation
                    )
            else:
                # The case that the latest statement is not in response to another statement
                return latest_statement

        return None

    class ChatBotException(Exception):
        pass



File: chatterbot/utils.py

"""
ChatterBot utility functions
"""


def import_module(dotted_path):
    """
    Imports the specified module based on the
    dot notated import path for the module.
    """
    import importlib

    module_parts = dotted_path.split('.')
    module_path = '.'.join(module_parts[:-1])
    module = importlib.import_module(module_path)

    return getattr(module, module_parts[-1])


def initialize_class(data, *args, **kwargs):
    """
    :param data: A string or dictionary containing a import_path attribute.
    """
    if isinstance(data, dict):
        import_path = data.get('import_path')
        data.update(kwargs)
        Class = import_module(import_path)

        return Class(*args, **data)
    else:
        Class = import_module(data)

        return Class(*args, **kwargs)


def validate_adapter_class(validate_class, adapter_class):
    """
    Raises an exception if validate_class is not a
    subclass of adapter_class.

    :param validate_class: The class to be validated.
    :type validate_class: class

    :param adapter_class: The class type to check against.
    :type adapter_class: class

    :raises: Adapter.InvalidAdapterTypeException
    """
    from chatterbot.adapters import Adapter

    # If a dictionary was passed in, check if it has an import_path attribute
    if isinstance(validate_class, dict):

        if 'import_path' not in validate_class:
            raise Adapter.InvalidAdapterTypeException(
                'The dictionary {} must contain a value for "import_path"'.format(
                    str(validate_class)
                )
            )

        # Set the class to the import path for the next check
        validate_class = validate_class.get('import_path')

    if not issubclass(import_module(validate_class), adapter_class):
        raise Adapter.InvalidAdapterTypeException(
            '{} must be a subclass of {}'.format(
                validate_class,
                adapter_class.__name__
            )
        )


def get_response_time(chatbot, statement='Hello'):
    """
    Returns the amount of time taken for a given
    chat bot to return a response.

    :param chatbot: A chat bot instance.
    :type chatbot: ChatBot

    :returns: The response time in seconds.
    :rtype: float
    """
    import time

    start_time = time.time()

    chatbot.get_response(statement)

    return time.time() - start_time


def print_progress_bar(description, iteration_counter, total_items, progress_bar_length=20):
    """
    Print progress bar
    :param description: Training description
    :type description: str

    :param iteration_counter: Incremental counter
    :type iteration_counter: int

    :param total_items: total number items
    :type total_items: int

    :param progress_bar_length: Progress bar length
    :type progress_bar_length: int

    :returns: void
    :rtype: void
    """
    import sys

    percent = float(iteration_counter) / total_items
    hashes = '#' * int(round(percent * progress_bar_length))
    spaces = ' ' * (progress_bar_length - len(hashes))
    sys.stdout.write('\r{0}: [{1}] {2}%'.format(description, hashes + spaces, int(round(percent * 100))))
    sys.stdout.flush()
    if total_items == iteration_counter:
        print('\r')



File: chatterbot/preprocessors.py

"""
Statement pre-processors.
"""


def clean_whitespace(statement):
    """
    Remove any consecutive whitespace characters from the statement text.
    """
    import re

    # Replace linebreaks and tabs with spaces
    statement.text = statement.text.replace('\n', ' ').replace('\r', ' ').replace('\t', ' ')

    # Remove any leeding or trailing whitespace
    statement.text = statement.text.strip()

    # Remove consecutive spaces
    statement.text = re.sub(' +', ' ', statement.text)

    return statement


def unescape_html(statement):
    """
    Convert escaped html characters into unescaped html characters.
    For example: "&lt;b&gt;" becomes "<b>".
    """
    import html

    statement.text = html.unescape(statement.text)

    return statement


def convert_to_ascii(statement):
    """
    Converts unicode characters to ASCII character equivalents.
    For example: "på fédéral" becomes "pa federal".
    """
    import unicodedata

    text = unicodedata.normalize('NFKD', statement.text)
    text = text.encode('ascii', 'ignore').decode('utf-8')

    statement.text = str(text)
    return statement



File: chatterbot/trainers.py

import os
import sys
import csv
import time
from dateutil import parser as date_parser
from chatterbot.conversation import Statement
from chatterbot.tagging import PosLemmaTagger
from chatterbot import utils


class Trainer(object):
    """
    Base class for all other trainer classes.

    :param boolean show_training_progress: Show progress indicators for the
           trainer. The environment variable ``CHATTERBOT_SHOW_TRAINING_PROGRESS``
           can also be set to control this. ``show_training_progress`` will override
           the environment variable if it is set.
    """

    def __init__(self, chatbot, **kwargs):
        self.chatbot = chatbot

        environment_default = os.getenv('CHATTERBOT_SHOW_TRAINING_PROGRESS', True)
        self.show_training_progress = kwargs.get(
            'show_training_progress',
            environment_default
        )

    def get_preprocessed_statement(self, input_statement):
        """
        Preprocess the input statement.
        """
        for preprocessor in self.chatbot.preprocessors:
            input_statement = preprocessor(input_statement)

        return input_statement

    def train(self, *args, **kwargs):
        """
        This method must be overridden by a child class.
        """
        raise self.TrainerInitializationException()

    class TrainerInitializationException(Exception):
        """
        Exception raised when a base class has not overridden
        the required methods on the Trainer base class.
        """

        def __init__(self, message=None):
            default = (
                'A training class must be specified before calling train(). '
                'See http://chatterbot.readthedocs.io/en/stable/training.html'
            )
            super().__init__(message or default)

    def _generate_export_data(self):
        result = []
        for statement in self.chatbot.storage.filter():
            if statement.in_response_to:
                result.append([statement.in_response_to, statement.text])

        return result

    def export_for_training(self, file_path='./export.json'):
        """
        Create a file from the database that can be used to
        train other chat bots.
        """
        import json
        export = {'conversations': self._generate_export_data()}
        with open(file_path, 'w+', encoding='utf8') as jsonfile:
            json.dump(export, jsonfile, ensure_ascii=False)


class ListTrainer(Trainer):
    """
    Allows a chat bot to be trained using a list of strings
    where the list represents a conversation.
    """

    def train(self, conversation):
        """
        Train the chat bot based on the provided list of
        statements that represents a single conversation.
        """
        previous_statement_text = None
        previous_statement_search_text = ''

        statements_to_create = []

        for conversation_count, text in enumerate(conversation):
            if self.show_training_progress:
                utils.print_progress_bar(
                    'List Trainer',
                    conversation_count + 1, len(conversation)
                )

            statement_search_text = self.chatbot.storage.tagger.get_text_index_string(text)

            statement = self.get_preprocessed_statement(
                Statement(
                    text=text,
                    search_text=statement_search_text,
                    in_response_to=previous_statement_text,
                    search_in_response_to=previous_statement_search_text,
                    conversation='training'
                )
            )

            previous_statement_text = statement.text
            previous_statement_search_text = statement_search_text

            statements_to_create.append(statement)

        self.chatbot.storage.create_many(statements_to_create)


class ChatterBotCorpusTrainer(Trainer):
    """
    Allows the chat bot to be trained using data from the
    ChatterBot dialog corpus.
    """

    def train(self, *corpus_paths):
        from chatterbot.corpus import load_corpus, list_corpus_files

        data_file_paths = []

        # Get the paths to each file the bot will be trained with
        for corpus_path in corpus_paths:
            data_file_paths.extend(list_corpus_files(corpus_path))

        for corpus, categories, file_path in load_corpus(*data_file_paths):

            statements_to_create = []

            # Train the chat bot with each statement and response pair
            for conversation_count, conversation in enumerate(corpus):

                if self.show_training_progress:
                    utils.print_progress_bar(
                        'Training ' + str(os.path.basename(file_path)),
                        conversation_count + 1,
                        len(corpus)
                    )

                previous_statement_text = None
                previous_statement_search_text = ''

                for text in conversation:

                    statement_search_text = self.chatbot.storage.tagger.get_text_index_string(text)

                    statement = Statement(
                        text=text,
                        search_text=statement_search_text,
                        in_response_to=previous_statement_text,
                        search_in_response_to=previous_statement_search_text,
                        conversation='training'
                    )

                    statement.add_tags(*categories)

                    statement = self.get_preprocessed_statement(statement)

                    previous_statement_text = statement.text
                    previous_statement_search_text = statement_search_text

                    statements_to_create.append(statement)

            if statements_to_create:
                self.chatbot.storage.create_many(statements_to_create)


class UbuntuCorpusTrainer(Trainer):
    """
    Allow chatbots to be trained with the data from the Ubuntu Dialog Corpus.
    """

    def __init__(self, chatbot, **kwargs):
        super().__init__(chatbot, **kwargs)
        home_directory = os.path.expanduser('~')

        self.data_download_url = kwargs.get(
            'ubuntu_corpus_data_download_url',
            'http://cs.mcgill.ca/~jpineau/datasets/ubuntu-corpus-1.0/ubuntu_dialogs.tgz'
        )

        self.data_directory = kwargs.get(
            'ubuntu_corpus_data_directory',
            os.path.join(home_directory, 'ubuntu_data')
        )

        self.extracted_data_directory = os.path.join(
            self.data_directory, 'ubuntu_dialogs'
        )

        # Create the data directory if it does not already exist
        if not os.path.exists(self.data_directory):
            os.makedirs(self.data_directory)

    def is_downloaded(self, file_path):
        """
        Check if the data file is already downloaded.
        """
        if os.path.exists(file_path):
            self.chatbot.logger.info('File is already downloaded')
            return True

        return False

    def is_extracted(self, file_path):
        """
        Check if the data file is already extracted.
        """

        if os.path.isdir(file_path):
            self.chatbot.logger.info('File is already extracted')
            return True
        return False

    def download(self, url, show_status=True):
        """
        Download a file from the given url.
        Show a progress indicator for the download status.
        Based on: http://stackoverflow.com/a/15645088/1547223
        """
        import requests

        file_name = url.split('/')[-1]
        file_path = os.path.join(self.data_directory, file_name)

        # Do not download the data if it already exists
        if self.is_downloaded(file_path):
            return file_path

        with open(file_path, 'wb') as open_file:
            print('Downloading %s' % url)
            response = requests.get(url, stream=True)
            total_length = response.headers.get('content-length')

            if total_length is None:
                # No content length header
                open_file.write(response.content)
            else:
                download = 0
                total_length = int(total_length)
                for data in response.iter_content(chunk_size=4096):
                    download += len(data)
                    open_file.write(data)
                    if show_status:
                        done = int(50 * download / total_length)
                        sys.stdout.write('\r[%s%s]' % ('=' * done, ' ' * (50 - done)))
                        sys.stdout.flush()

            # Add a new line after the download bar
            sys.stdout.write('\n')

        print('Download location: %s' % file_path)
        return file_path

    def extract(self, file_path):
        """
        Extract a tar file at the specified file path.
        """
        import tarfile

        print('Extracting {}'.format(file_path))

        if not os.path.exists(self.extracted_data_directory):
            os.makedirs(self.extracted_data_directory)

        def track_progress(members):
            sys.stdout.write('.')
            for member in members:
                # This will be the current file being extracted
                yield member

        with tarfile.open(file_path) as tar:
            tar.extractall(path=self.extracted_data_directory, members=track_progress(tar))

        self.chatbot.logger.info('File extracted to {}'.format(self.extracted_data_directory))

        return True

    def train(self):
        import glob

        tagger = PosLemmaTagger(language=self.chatbot.storage.tagger.language)

        # Download and extract the Ubuntu dialog corpus if needed
        corpus_download_path = self.download(self.data_download_url)

        # Extract if the directory does not already exist
        if not self.is_extracted(self.extracted_data_directory):
            self.extract(corpus_download_path)

        extracted_corpus_path = os.path.join(
            self.extracted_data_directory,
            '**', '**', '*.tsv'
        )

        def chunks(items, items_per_chunk):
            for start_index in range(0, len(items), items_per_chunk):
                end_index = start_index + items_per_chunk
                yield items[start_index:end_index]

        file_list = glob.glob(extracted_corpus_path)

        file_groups = tuple(chunks(file_list, 10000))

        start_time = time.time()

        for tsv_files in file_groups:

            statements_from_file = []

            for tsv_file in tsv_files:
                with open(tsv_file, 'r', encoding='utf-8') as tsv:
                    reader = csv.reader(tsv, delimiter='\t')

                    previous_statement_text = None
                    previous_statement_search_text = ''

                    for row in reader:
                        if len(row) > 0:
                            statement = Statement(
                                text=row[3],
                                in_response_to=previous_statement_text,
                                conversation='training',
                                created_at=date_parser.parse(row[0]),
                                persona=row[1]
                            )

                            for preprocessor in self.chatbot.preprocessors:
                                statement = preprocessor(statement)

                            statement.search_text = tagger.get_text_index_string(statement.text)
                            statement.search_in_response_to = previous_statement_search_text

                            previous_statement_text = statement.text
                            previous_statement_search_text = statement.search_text

                            statements_from_file.append(statement)

            self.chatbot.storage.create_many(statements_from_file)

        print('Training took', time.time() - start_time, 'seconds.')



File: chatterbot/search.py

class IndexedTextSearch:
    """
    :param statement_comparison_function: A comparison class.
        Defaults to ``LevenshteinDistance``.

    :param search_page_size:
        The maximum number of records to load into memory at a time when searching.
        Defaults to 1000
    """

    name = 'indexed_text_search'

    def __init__(self, chatbot, **kwargs):
        from chatterbot.comparisons import LevenshteinDistance

        self.chatbot = chatbot

        statement_comparison_function = kwargs.get(
            'statement_comparison_function',
            LevenshteinDistance
        )

        self.compare_statements = statement_comparison_function(
            language=self.chatbot.storage.tagger.language
        )

        self.search_page_size = kwargs.get(
            'search_page_size', 1000
        )

    def search(self, input_statement, **additional_parameters):
        """
        Search for close matches to the input. Confidence scores for
        subsequent results will order of increasing value.

        :param input_statement: A statement.
        :type input_statement: chatterbot.conversation.Statement

        :param **additional_parameters: Additional parameters to be passed
            to the ``filter`` method of the storage adapter when searching.

        :rtype: Generator yielding one closest matching statement at a time.
        """
        self.chatbot.logger.info('Beginning search for close text match')

        input_search_text = input_statement.search_text

        if not input_statement.search_text:
            self.chatbot.logger.warn(
                'No value for search_text was available on the provided input'
            )

            input_search_text = self.chatbot.storage.tagger.get_text_index_string(
                input_statement.text
            )

        search_parameters = {
            'search_text_contains': input_search_text,
            'persona_not_startswith': 'bot:',
            'page_size': self.search_page_size
        }

        if additional_parameters:
            search_parameters.update(additional_parameters)

        statement_list = self.chatbot.storage.filter(**search_parameters)

        best_confidence_so_far = 0

        self.chatbot.logger.info('Processing search results')

        # Find the closest matching known statement
        for statement in statement_list:
            confidence = self.compare_statements(input_statement, statement)

            if confidence > best_confidence_so_far:
                best_confidence_so_far = confidence
                statement.confidence = confidence

                self.chatbot.logger.info('Similar text found: {} {}'.format(
                    statement.text, confidence
                ))

                yield statement


class TextSearch:
    """
    :param statement_comparison_function: A comparison class.
        Defaults to ``LevenshteinDistance``.

    :param search_page_size:
        The maximum number of records to load into memory at a time when searching.
        Defaults to 1000
    """

    name = 'text_search'

    def __init__(self, chatbot, **kwargs):
        from chatterbot.comparisons import LevenshteinDistance

        self.chatbot = chatbot

        statement_comparison_function = kwargs.get(
            'statement_comparison_function',
            LevenshteinDistance
        )

        self.compare_statements = statement_comparison_function(
            language=self.chatbot.storage.tagger.language
        )

        self.search_page_size = kwargs.get(
            'search_page_size', 1000
        )

    def search(self, input_statement, **additional_parameters):
        """
        Search for close matches to the input. Confidence scores for
        subsequent results will order of increasing value.

        :param input_statement: A statement.
        :type input_statement: chatterbot.conversation.Statement

        :param **additional_parameters: Additional parameters to be passed
            to the ``filter`` method of the storage adapter when searching.

        :rtype: Generator yielding one closest matching statement at a time.
        """
        self.chatbot.logger.info('Beginning search for close text match')

        search_parameters = {
            'persona_not_startswith': 'bot:',
            'page_size': self.search_page_size
        }

        if additional_parameters:
            search_parameters.update(additional_parameters)

        statement_list = self.chatbot.storage.filter(**search_parameters)

        best_confidence_so_far = 0

        self.chatbot.logger.info('Processing search results')

        # Find the closest matching known statement
        for statement in statement_list:
            confidence = self.compare_statements(input_statement, statement)

            if confidence > best_confidence_so_far:
                best_confidence_so_far = confidence
                statement.confidence = confidence

                self.chatbot.logger.info('Similar text found: {} {}'.format(
                    statement.text, confidence
                ))

                yield statement



File: chatterbot/exceptions.py

class OptionalDependencyImportError(ImportError):
    """
    An exception raised when a feature requires an optional dependency to be installed.
    """
    pass



File: chatterbot/tagging.py

import string
from chatterbot import languages


class LowercaseTagger(object):
    """
    Returns the text in lowercase.
    """

    def __init__(self, language=None):
        self.language = language or languages.ENG

    def get_text_index_string(self, text):
        return text.lower()


class PosLemmaTagger(object):

    def __init__(self, language=None):
        import spacy

        self.language = language or languages.ENG

        self.punctuation_table = str.maketrans(dict.fromkeys(string.punctuation))

        self.nlp = spacy.load(self.language.ISO_639_1.lower())

    def get_text_index_string(self, text):
        """
        Return a string of text containing part-of-speech, lemma pairs.
        """
        bigram_pairs = []

        if len(text) <= 2:
            text_without_punctuation = text.translate(self.punctuation_table)
            if len(text_without_punctuation) >= 1:
                text = text_without_punctuation

        document = self.nlp(text)

        if len(text) <= 2:
            bigram_pairs = [
                token.lemma_.lower() for token in document
            ]
        else:
            tokens = [
                token for token in document if token.is_alpha and not token.is_stop
            ]

            if len(tokens) < 2:
                tokens = [
                    token for token in document if token.is_alpha
                ]

            for index in range(1, len(tokens)):
                bigram_pairs.append('{}:{}'.format(
                    tokens[index - 1].pos_,
                    tokens[index].lemma_.lower()
                ))

        if not bigram_pairs:
            bigram_pairs = [
                token.lemma_.lower() for token in document
            ]

        return ' '.join(bigram_pairs)



File: chatterbot/languages.py

class AAR:
    ISO_639_1 = ''
    ISO_639 = 'aar'
    ENGLISH_NAME = 'Afar'


class ABK:
    ISO_639_1 = ''
    ISO_639 = 'abk'
    ENGLISH_NAME = 'Abkhazian'


class ACE:
    ISO_639_1 = ''
    ISO_639 = 'ace'
    ENGLISH_NAME = 'Achinese'


class ACH:
    ISO_639_1 = ''
    ISO_639 = 'ach'
    ENGLISH_NAME = 'Acoli'


class ADA:
    ISO_639_1 = ''
    ISO_639 = 'ada'
    ENGLISH_NAME = 'Adangme'


class ADY:
    ISO_639_1 = ''
    ISO_639 = 'ady'
    ENGLISH_NAME = 'Adyghe'


class AFH:
    ISO_639_1 = ''
    ISO_639 = 'afh'
    ENGLISH_NAME = 'Afrihili'


class AFR:
    ISO_639_1 = ''
    ISO_639 = 'afr'
    ENGLISH_NAME = 'Afrikaans'


class AIN:
    ISO_639_1 = ''
    ISO_639 = 'ain'
    ENGLISH_NAME = 'Ainu'


class AKA:
    ISO_639_1 = ''
    ISO_639 = 'aka'
    ENGLISH_NAME = 'Akan'


class AKK:
    ISO_639_1 = ''
    ISO_639 = 'akk'
    ENGLISH_NAME = 'Akkadian'


class ALB:
    ISO_639_1 = ''
    ISO_639 = 'alb'
    ENGLISH_NAME = 'Albanian'


class ALE:
    ISO_639_1 = ''
    ISO_639 = 'ale'
    ENGLISH_NAME = 'Aleut'


class ALT:
    ISO_639_1 = ''
    ISO_639 = 'alt'
    ENGLISH_NAME = 'SouthernAltai'


class AMH:
    ISO_639_1 = ''
    ISO_639 = 'amh'
    ENGLISH_NAME = 'Amharic'


class ANP:
    ISO_639_1 = ''
    ISO_639 = 'anp'
    ENGLISH_NAME = 'Angika'


class ARA:
    ISO_639_1 = ''
    ISO_639 = 'ara'
    ENGLISH_NAME = 'Arabic'


class ARG:
    ISO_639_1 = ''
    ISO_639 = 'arg'
    ENGLISH_NAME = 'Aragonese'


class ARM:
    ISO_639_1 = ''
    ISO_639 = 'arm'
    ENGLISH_NAME = 'Armenian'


class ARN:
    ISO_639_1 = ''
    ISO_639 = 'arn'
    ENGLISH_NAME = 'Mapudungun'


class ARP:
    ISO_639_1 = ''
    ISO_639 = 'arp'
    ENGLISH_NAME = 'Arapaho'


class ARW:
    ISO_639_1 = ''
    ISO_639 = 'arw'
    ENGLISH_NAME = 'Arawak'


class ASM:
    ISO_639_1 = ''
    ISO_639 = 'asm'
    ENGLISH_NAME = 'Assamese'


class AST:
    ISO_639_1 = ''
    ISO_639 = 'ast'
    ENGLISH_NAME = 'Asturian'


class AVA:
    ISO_639_1 = ''
    ISO_639 = 'ava'
    ENGLISH_NAME = 'Avaric'


class AVE:
    ISO_639_1 = ''
    ISO_639 = 'ave'
    ENGLISH_NAME = 'Avestan'


class AWA:
    ISO_639_1 = ''
    ISO_639 = 'awa'
    ENGLISH_NAME = 'Awadhi'


class AYM:
    ISO_639_1 = ''
    ISO_639 = 'aym'
    ENGLISH_NAME = 'Aymara'


class AZE:
    ISO_639_1 = ''
    ISO_639 = 'aze'
    ENGLISH_NAME = 'Azerbaijani'


class BAK:
    ISO_639_1 = ''
    ISO_639 = 'bak'
    ENGLISH_NAME = 'Bashkir'


class BAL:
    ISO_639_1 = ''
    ISO_639 = 'bal'
    ENGLISH_NAME = 'Baluchi'


class BAM:
    ISO_639_1 = ''
    ISO_639 = 'bam'
    ENGLISH_NAME = 'Bambara'


class BAN:
    ISO_639_1 = ''
    ISO_639 = 'ban'
    ENGLISH_NAME = 'Balinese'


class BAQ:
    ISO_639_1 = ''
    ISO_639 = 'baq'
    ENGLISH_NAME = 'Basque'


class BAS:
    ISO_639_1 = ''
    ISO_639 = 'bas'
    ENGLISH_NAME = 'Basa'


class BEJ:
    ISO_639_1 = ''
    ISO_639 = 'bej'
    ENGLISH_NAME = 'Beja'


class BEL:
    ISO_639_1 = ''
    ISO_639 = 'bel'
    ENGLISH_NAME = 'Belarusian'


class BEM:
    ISO_639_1 = ''
    ISO_639 = 'bem'
    ENGLISH_NAME = 'Bemba'


class BEN:
    ISO_639_1 = 'bn'
    ISO_639 = 'ben'
    ENGLISH_NAME = 'Bengali'


class BHO:
    ISO_639_1 = ''
    ISO_639 = 'bho'
    ENGLISH_NAME = 'Bhojpuri'


class BIK:
    ISO_639_1 = ''
    ISO_639 = 'bik'
    ENGLISH_NAME = 'Bikol'


class BIN:
    ISO_639_1 = ''
    ISO_639 = 'bin'
    ENGLISH_NAME = 'Bini'


class BIS:
    ISO_639_1 = ''
    ISO_639 = 'bis'
    ENGLISH_NAME = 'Bislama'


class BLA:
    ISO_639_1 = ''
    ISO_639 = 'bla'
    ENGLISH_NAME = 'Siksika'


class BOS:
    ISO_639_1 = ''
    ISO_639 = 'bos'
    ENGLISH_NAME = 'Bosnian'


class BRA:
    ISO_639_1 = ''
    ISO_639 = 'bra'
    ENGLISH_NAME = 'Braj'


class BRE:
    ISO_639_1 = ''
    ISO_639 = 'bre'
    ENGLISH_NAME = 'Breton'


class BUA:
    ISO_639_1 = ''
    ISO_639 = 'bua'
    ENGLISH_NAME = 'Buriat'


class BUG:
    ISO_639_1 = ''
    ISO_639 = 'bug'
    ENGLISH_NAME = 'Buginese'


class BUL:
    ISO_639_1 = ''
    ISO_639 = 'bul'
    ENGLISH_NAME = 'Bulgarian'


class BUR:
    ISO_639_1 = ''
    ISO_639 = 'bur'
    ENGLISH_NAME = 'Burmese'


class BYN:
    ISO_639_1 = ''
    ISO_639 = 'byn'
    ENGLISH_NAME = 'Blin'


class CAD:
    ISO_639_1 = ''
    ISO_639 = 'cad'
    ENGLISH_NAME = 'Caddo'


class CAR:
    ISO_639_1 = ''
    ISO_639 = 'car'
    ENGLISH_NAME = 'GalibiCarib'


class CAT:
    ISO_639_1 = ''
    ISO_639 = 'cat'
    ENGLISH_NAME = 'Catalan'


class CEB:
    ISO_639_1 = ''
    ISO_639 = 'ceb'
    ENGLISH_NAME = 'Cebuano'


class CHA:
    ISO_639_1 = ''
    ISO_639 = 'cha'
    ENGLISH_NAME = 'Chamorro'


class CHB:
    ISO_639_1 = ''
    ISO_639 = 'chb'
    ENGLISH_NAME = 'Chibcha'


class CHE:
    ISO_639_1 = ''
    ISO_639 = 'che'
    ENGLISH_NAME = 'Chechen'


class CHG:
    ISO_639_1 = ''
    ISO_639 = 'chg'
    ENGLISH_NAME = 'Chagatai'


class CHI:
    ISO_639_1 = 'zh'
    ISO_639 = 'chi'
    ENGLISH_NAME = 'Chinese'


class CHK:
    ISO_639_1 = ''
    ISO_639 = 'chk'
    ENGLISH_NAME = 'Chuukese'


class CHM:
    ISO_639_1 = ''
    ISO_639 = 'chm'
    ENGLISH_NAME = 'Mari'


class CHN:
    ISO_639_1 = ''
    ISO_639 = 'chn'
    ENGLISH_NAME = 'Chinookjargon'


class CHO:
    ISO_639_1 = ''
    ISO_639 = 'cho'
    ENGLISH_NAME = 'Choctaw'


class CHP:
    ISO_639_1 = ''
    ISO_639 = 'chp'
    ENGLISH_NAME = 'Chipewyan'


class CHR:
    ISO_639_1 = ''
    ISO_639 = 'chr'
    ENGLISH_NAME = 'Cherokee'


class CHV:
    ISO_639_1 = ''
    ISO_639 = 'chv'
    ENGLISH_NAME = 'Chuvash'


class CHY:
    ISO_639_1 = ''
    ISO_639 = 'chy'
    ENGLISH_NAME = 'Cheyenne'


class CNR:
    ISO_639_1 = ''
    ISO_639 = 'cnr'
    ENGLISH_NAME = 'Montenegrin'


class COP:
    ISO_639_1 = ''
    ISO_639 = 'cop'
    ENGLISH_NAME = 'Coptic'


class COR:
    ISO_639_1 = ''
    ISO_639 = 'cor'
    ENGLISH_NAME = 'Cornish'


class COS:
    ISO_639_1 = ''
    ISO_639 = 'cos'
    ENGLISH_NAME = 'Corsican'


class CPE:
    ISO_639_1 = ''
    ISO_639 = 'cpe'
    ENGLISH_NAME = 'Creolesandpidgins'


class CPF:
    ISO_639_1 = ''
    ISO_639 = 'cpf'
    ENGLISH_NAME = 'Creolesandpidgins'


class CPP:
    ISO_639_1 = ''
    ISO_639 = 'cpp'
    ENGLISH_NAME = 'Creolesandpidgins'


class CRE:
    ISO_639_1 = ''
    ISO_639 = 'cre'
    ENGLISH_NAME = 'Cree'


class CRH:
    ISO_639_1 = ''
    ISO_639 = 'crh'
    ENGLISH_NAME = 'CrimeanTatar'


class CRP:
    ISO_639_1 = ''
    ISO_639 = 'crp'
    ENGLISH_NAME = 'Creolesandpidgins'


class CSB:
    ISO_639_1 = ''
    ISO_639 = 'csb'
    ENGLISH_NAME = 'Kashubian'


class CZE:
    ISO_639_1 = ''
    ISO_639 = 'cze'
    ENGLISH_NAME = 'Czech'


class DAK:
    ISO_639_1 = ''
    ISO_639 = 'dak'
    ENGLISH_NAME = 'Dakota'


class DAN:
    ISO_639_1 = ''
    ISO_639 = 'dan'
    ENGLISH_NAME = 'Danish'


class DAR:
    ISO_639_1 = ''
    ISO_639 = 'dar'
    ENGLISH_NAME = 'Dargwa'


class DEL:
    ISO_639_1 = ''
    ISO_639 = 'del'
    ENGLISH_NAME = 'Delaware'


class DEN:
    ISO_639_1 = ''
    ISO_639 = 'den'
    ENGLISH_NAME = 'Slave'


class DGR:
    ISO_639_1 = ''
    ISO_639 = 'dgr'
    ENGLISH_NAME = 'Dogrib'


class DIN:
    ISO_639_1 = ''
    ISO_639 = 'din'
    ENGLISH_NAME = 'Dinka'


class DIV:
    ISO_639_1 = ''
    ISO_639 = 'div'
    ENGLISH_NAME = 'Divehi'


class DOI:
    ISO_639_1 = ''
    ISO_639 = 'doi'
    ENGLISH_NAME = 'Dogri'


class DUA:
    ISO_639_1 = ''
    ISO_639 = 'dua'
    ENGLISH_NAME = 'Duala'


class DUT:
    ISO_639_1 = 'nl'
    ISO_639 = 'dut'
    ENGLISH_NAME = 'Dutch'


class DYU:
    ISO_639_1 = ''
    ISO_639 = 'dyu'
    ENGLISH_NAME = 'Dyula'


class DZO:
    ISO_639_1 = ''
    ISO_639 = 'dzo'
    ENGLISH_NAME = 'Dzongkha'


class EFI:
    ISO_639_1 = ''
    ISO_639 = 'efi'
    ENGLISH_NAME = 'Efik'


class EKA:
    ISO_639_1 = ''
    ISO_639 = 'eka'
    ENGLISH_NAME = 'Ekajuk'


class ELX:
    ISO_639_1 = ''
    ISO_639 = 'elx'
    ENGLISH_NAME = 'Elamite'


class ENG:
    ISO_639_1 = 'en'
    ISO_639 = 'eng'
    ENGLISH_NAME = 'English'


class EPO:
    ISO_639_1 = ''
    ISO_639 = 'epo'
    ENGLISH_NAME = 'Esperanto'


class EST:
    ISO_639_1 = ''
    ISO_639 = 'est'
    ENGLISH_NAME = 'Estonian'


class EWE:
    ISO_639_1 = ''
    ISO_639 = 'ewe'
    ENGLISH_NAME = 'Ewe'


class EWO:
    ISO_639_1 = ''
    ISO_639 = 'ewo'
    ENGLISH_NAME = 'Ewondo'


class FAN:
    ISO_639_1 = ''
    ISO_639 = 'fan'
    ENGLISH_NAME = 'Fang'


class FAO:
    ISO_639_1 = ''
    ISO_639 = 'fao'
    ENGLISH_NAME = 'Faroese'


class FAT:
    ISO_639_1 = ''
    ISO_639 = 'fat'
    ENGLISH_NAME = 'Fanti'


class FIJ:
    ISO_639_1 = ''
    ISO_639 = 'fij'
    ENGLISH_NAME = 'Fijian'


class FIL:
    ISO_639_1 = ''
    ISO_639 = 'fil'
    ENGLISH_NAME = 'Filipino'


class FIN:
    ISO_639_1 = ''
    ISO_639 = 'fin'
    ENGLISH_NAME = 'Finnish'


class FON:
    ISO_639_1 = ''
    ISO_639 = 'fon'
    ENGLISH_NAME = 'Fon'


class FRE:
    ISO_639_1 = ''
    ISO_639 = 'fre'
    ENGLISH_NAME = 'French'


class FRR:
    ISO_639_1 = ''
    ISO_639 = 'frr'
    ENGLISH_NAME = 'NorthernFrisian'


class FRS:
    ISO_639_1 = ''
    ISO_639 = 'frs'
    ENGLISH_NAME = 'EasternFrisian'


class FRY:
    ISO_639_1 = ''
    ISO_639 = 'fry'
    ENGLISH_NAME = 'WesternFrisian'


class FUL:
    ISO_639_1 = ''
    ISO_639 = 'ful'
    ENGLISH_NAME = 'Fulah'


class FUR:
    ISO_639_1 = ''
    ISO_639 = 'fur'
    ENGLISH_NAME = 'Friulian'


class GAA:
    ISO_639_1 = ''
    ISO_639 = 'gaa'
    ENGLISH_NAME = 'Ga'


class GAY:
    ISO_639_1 = ''
    ISO_639 = 'gay'
    ENGLISH_NAME = 'Gayo'


class GBA:
    ISO_639_1 = ''
    ISO_639 = 'gba'
    ENGLISH_NAME = 'Gbaya'


class GEO:
    ISO_639_1 = ''
    ISO_639 = 'geo'
    ENGLISH_NAME = 'Georgian'


class GER:
    ISO_639_1 = 'de'
    ISO_639 = 'ger'
    ENGLISH_NAME = 'German'


class GEZ:
    ISO_639_1 = ''
    ISO_639 = 'gez'
    ENGLISH_NAME = 'Geez'


class GIL:
    ISO_639_1 = ''
    ISO_639 = 'gil'
    ENGLISH_NAME = 'Gilbertese'


class GLA:
    ISO_639_1 = ''
    ISO_639 = 'gla'
    ENGLISH_NAME = 'Gaelic'


class GLE:
    ISO_639_1 = ''
    ISO_639 = 'gle'
    ENGLISH_NAME = 'Irish'


class GLG:
    ISO_639_1 = ''
    ISO_639 = 'glg'
    ENGLISH_NAME = 'Galician'


class GLV:
    ISO_639_1 = ''
    ISO_639 = 'glv'
    ENGLISH_NAME = 'Manx'


class GON:
    ISO_639_1 = ''
    ISO_639 = 'gon'
    ENGLISH_NAME = 'Gondi'


class GOR:
    ISO_639_1 = ''
    ISO_639 = 'gor'
    ENGLISH_NAME = 'Gorontalo'


class GOT:
    ISO_639_1 = ''
    ISO_639 = 'got'
    ENGLISH_NAME = 'Gothic'


class GRB:
    ISO_639_1 = ''
    ISO_639 = 'grb'
    ENGLISH_NAME = 'Grebo'


class GRE:
    ISO_639_1 = 'el'
    ISO_639 = 'gre'
    ENGLISH_NAME = 'Greek'


class GRN:
    ISO_639_1 = ''
    ISO_639 = 'grn'
    ENGLISH_NAME = 'Guarani'


class GSW:
    ISO_639_1 = ''
    ISO_639 = 'gsw'
    ENGLISH_NAME = 'SwissGerman'


class GUJ:
    ISO_639_1 = ''
    ISO_639 = 'guj'
    ENGLISH_NAME = 'Gujarati'


class GWI:
    ISO_639_1 = ''
    ISO_639 = 'gwi'
    ENGLISH_NAME = 'Gwichin'


class HAI:
    ISO_639_1 = ''
    ISO_639 = 'hai'
    ENGLISH_NAME = 'Haida'


class HAT:
    ISO_639_1 = ''
    ISO_639 = 'hat'
    ENGLISH_NAME = 'Haitian'


class HAU:
    ISO_639_1 = ''
    ISO_639 = 'hau'
    ENGLISH_NAME = 'Hausa'


class HAW:
    ISO_639_1 = ''
    ISO_639 = 'haw'
    ENGLISH_NAME = 'Hawaiian'


class HEB:
    ISO_639_1 = 'he'
    ISO_639 = 'heb'
    ENGLISH_NAME = 'Hebrew'


class HER:
    ISO_639_1 = ''
    ISO_639 = 'her'
    ENGLISH_NAME = 'Herero'


class HIL:
    ISO_639_1 = ''
    ISO_639 = 'hil'
    ENGLISH_NAME = 'Hiligaynon'


class HIN:
    ISO_639_1 = 'hi'
    ISO_639 = 'hin'
    ENGLISH_NAME = 'Hindi'


class HIT:
    ISO_639_1 = ''
    ISO_639 = 'hit'
    ENGLISH_NAME = 'Hittite'


class HMN:
    ISO_639_1 = ''
    ISO_639 = 'hmn'
    ENGLISH_NAME = 'Hmong'


class HMO:
    ISO_639_1 = ''
    ISO_639 = 'hmo'
    ENGLISH_NAME = 'HiriMotu'


class HRV:
    ISO_639_1 = ''
    ISO_639 = 'hrv'
    ENGLISH_NAME = 'Croatian'


class HSB:
    ISO_639_1 = ''
    ISO_639 = 'hsb'
    ENGLISH_NAME = 'UpperSorbian'


class HUN:
    ISO_639_1 = ''
    ISO_639 = 'hun'
    ENGLISH_NAME = 'Hungarian'


class HUP:
    ISO_639_1 = ''
    ISO_639 = 'hup'
    ENGLISH_NAME = 'Hupa'


class IBA:
    ISO_639_1 = ''
    ISO_639 = 'iba'
    ENGLISH_NAME = 'Iban'


class IBO:
    ISO_639_1 = ''
    ISO_639 = 'ibo'
    ENGLISH_NAME = 'Igbo'


class ICE:
    ISO_639_1 = ''
    ISO_639 = 'ice'
    ENGLISH_NAME = 'Icelandic'


class IDO:
    ISO_639_1 = ''
    ISO_639 = 'ido'
    ENGLISH_NAME = 'Ido'


class III:
    ISO_639_1 = ''
    ISO_639 = 'iii'
    ENGLISH_NAME = 'SichuanYi'


class IKU:
    ISO_639_1 = ''
    ISO_639 = 'iku'
    ENGLISH_NAME = 'Inuktitut'


class ILE:
    ISO_639_1 = ''
    ISO_639 = 'ile'
    ENGLISH_NAME = 'Interlingue'


class ILO:
    ISO_639_1 = ''
    ISO_639 = 'ilo'
    ENGLISH_NAME = 'Iloko'


class INA:
    ISO_639_1 = ''
    ISO_639 = 'ina'
    ENGLISH_NAME = 'Interlingua'


class IND:
    ISO_639_1 = 'id'
    ISO_639 = 'ind'
    ENGLISH_NAME = 'Indonesian'


class INH:
    ISO_639_1 = ''
    ISO_639 = 'inh'
    ENGLISH_NAME = 'Ingush'


class IPK:
    ISO_639_1 = ''
    ISO_639 = 'ipk'
    ENGLISH_NAME = 'Inupiaq'


class ITA:
    ISO_639_1 = ''
    ISO_639 = 'ita'
    ENGLISH_NAME = 'Italian'


class JAV:
    ISO_639_1 = ''
    ISO_639 = 'jav'
    ENGLISH_NAME = 'Javanese'


class JBO:
    ISO_639_1 = ''
    ISO_639 = 'jbo'
    ENGLISH_NAME = 'Lojban'


class JPN:
    ISO_639_1 = 'ja'
    ISO_639 = 'jpn'
    ENGLISH_NAME = 'Japanese'


class JPR:
    ISO_639_1 = ''
    ISO_639 = 'jpr'
    ENGLISH_NAME = 'JudeoPersian'


class JRB:
    ISO_639_1 = ''
    ISO_639 = 'jrb'
    ENGLISH_NAME = 'JudeoArabic'


class KAA:
    ISO_639_1 = ''
    ISO_639 = 'kaa'
    ENGLISH_NAME = 'KaraKalpak'


class KAB:
    ISO_639_1 = ''
    ISO_639 = 'kab'
    ENGLISH_NAME = 'Kabyle'


class KAC:
    ISO_639_1 = ''
    ISO_639 = 'kac'
    ENGLISH_NAME = 'Kachin'


class KAL:
    ISO_639_1 = ''
    ISO_639 = 'kal'
    ENGLISH_NAME = 'Kalaallisut'


class KAM:
    ISO_639_1 = ''
    ISO_639 = 'kam'
    ENGLISH_NAME = 'Kamba'


class KAN:
    ISO_639_1 = ''
    ISO_639 = 'kan'
    ENGLISH_NAME = 'Kannada'


class KAS:
    ISO_639_1 = ''
    ISO_639 = 'kas'
    ENGLISH_NAME = 'Kashmiri'


class KAU:
    ISO_639_1 = ''
    ISO_639 = 'kau'
    ENGLISH_NAME = 'Kanuri'


class KAW:
    ISO_639_1 = ''
    ISO_639 = 'kaw'
    ENGLISH_NAME = 'Kawi'


class KAZ:
    ISO_639_1 = ''
    ISO_639 = 'kaz'
    ENGLISH_NAME = 'Kazakh'


class KBD:
    ISO_639_1 = ''
    ISO_639 = 'kbd'
    ENGLISH_NAME = 'Kabardian'


class KHA:
    ISO_639_1 = ''
    ISO_639 = 'kha'
    ENGLISH_NAME = 'Khasi'


class KHM:
    ISO_639_1 = ''
    ISO_639 = 'khm'
    ENGLISH_NAME = 'CentralKhmer'


class KHO:
    ISO_639_1 = ''
    ISO_639 = 'kho'
    ENGLISH_NAME = 'Khotanese'


class KIK:
    ISO_639_1 = ''
    ISO_639 = 'kik'
    ENGLISH_NAME = 'Kikuyu'


class KIN:
    ISO_639_1 = ''
    ISO_639 = 'kin'
    ENGLISH_NAME = 'Kinyarwanda'


class KIR:
    ISO_639_1 = ''
    ISO_639 = 'kir'
    ENGLISH_NAME = 'Kirghiz'


class KMB:
    ISO_639_1 = ''
    ISO_639 = 'kmb'
    ENGLISH_NAME = 'Kimbundu'


class KOK:
    ISO_639_1 = ''
    ISO_639 = 'kok'
    ENGLISH_NAME = 'Konkani'


class KOM:
    ISO_639_1 = ''
    ISO_639 = 'kom'
    ENGLISH_NAME = 'Komi'


class KON:
    ISO_639_1 = ''
    ISO_639 = 'kon'
    ENGLISH_NAME = 'Kongo'


class KOR:
    ISO_639_1 = 'ko'
    ISO_639 = 'kor'
    ENGLISH_NAME = 'Korean'


class KOS:
    ISO_639_1 = ''
    ISO_639 = 'kos'
    ENGLISH_NAME = 'Kosraean'


class KPE:
    ISO_639_1 = ''
    ISO_639 = 'kpe'
    ENGLISH_NAME = 'Kpelle'


class KRC:
    ISO_639_1 = ''
    ISO_639 = 'krc'
    ENGLISH_NAME = 'KarachayBalkar'


class KRL:
    ISO_639_1 = ''
    ISO_639 = 'krl'
    ENGLISH_NAME = 'Karelian'


class KRU:
    ISO_639_1 = ''
    ISO_639 = 'kru'
    ENGLISH_NAME = 'Kurukh'


class KUA:
    ISO_639_1 = ''
    ISO_639 = 'kua'
    ENGLISH_NAME = 'Kuanyama'


class KUM:
    ISO_639_1 = ''
    ISO_639 = 'kum'
    ENGLISH_NAME = 'Kumyk'


class KUR:
    ISO_639_1 = ''
    ISO_639 = 'kur'
    ENGLISH_NAME = 'Kurdish'


class KUT:
    ISO_639_1 = ''
    ISO_639 = 'kut'
    ENGLISH_NAME = 'Kutenai'


class LAD:
    ISO_639_1 = ''
    ISO_639 = 'lad'
    ENGLISH_NAME = 'Ladino'


class LAH:
    ISO_639_1 = ''
    ISO_639 = 'lah'
    ENGLISH_NAME = 'Lahnda'


class LAM:
    ISO_639_1 = ''
    ISO_639 = 'lam'
    ENGLISH_NAME = 'Lamba'


class LAO:
    ISO_639_1 = ''
    ISO_639 = 'lao'
    ENGLISH_NAME = 'Lao'


class LAT:
    ISO_639_1 = ''
    ISO_639 = 'lat'
    ENGLISH_NAME = 'Latin'


class LAV:
    ISO_639_1 = ''
    ISO_639 = 'lav'
    ENGLISH_NAME = 'Latvian'


class LEZ:
    ISO_639_1 = ''
    ISO_639 = 'lez'
    ENGLISH_NAME = 'Lezghian'


class LIM:
    ISO_639_1 = ''
    ISO_639 = 'lim'
    ENGLISH_NAME = 'Limburgan'


class LIN:
    ISO_639_1 = ''
    ISO_639 = 'lin'
    ENGLISH_NAME = 'Lingala'


class LIT:
    ISO_639_1 = ''
    ISO_639 = 'lit'
    ENGLISH_NAME = 'Lithuanian'


class LOL:
    ISO_639_1 = ''
    ISO_639 = 'lol'
    ENGLISH_NAME = 'Mongo'


class LOZ:
    ISO_639_1 = ''
    ISO_639 = 'loz'
    ENGLISH_NAME = 'Lozi'


class LTZ:
    ISO_639_1 = ''
    ISO_639 = 'ltz'
    ENGLISH_NAME = 'Luxembourgish'


class LUA:
    ISO_639_1 = ''
    ISO_639 = 'lua'
    ENGLISH_NAME = 'LubaLulua'


class LUB:
    ISO_639_1 = ''
    ISO_639 = 'lub'
    ENGLISH_NAME = 'LubaKatanga'


class LUG:
    ISO_639_1 = ''
    ISO_639 = 'lug'
    ENGLISH_NAME = 'Ganda'


class LUI:
    ISO_639_1 = ''
    ISO_639 = 'lui'
    ENGLISH_NAME = 'Luiseno'


class LUN:
    ISO_639_1 = ''
    ISO_639 = 'lun'
    ENGLISH_NAME = 'Lunda'


class LUO:
    ISO_639_1 = ''
    ISO_639 = 'luo'
    ENGLISH_NAME = 'Luo'


class LUS:
    ISO_639_1 = ''
    ISO_639 = 'lus'
    ENGLISH_NAME = 'Lushai'


class MAC:
    ISO_639_1 = ''
    ISO_639 = 'mac'
    ENGLISH_NAME = 'Macedonian'


class MAD:
    ISO_639_1 = ''
    ISO_639 = 'mad'
    ENGLISH_NAME = 'Madurese'


class MAG:
    ISO_639_1 = ''
    ISO_639 = 'mag'
    ENGLISH_NAME = 'Magahi'


class MAH:
    ISO_639_1 = ''
    ISO_639 = 'mah'
    ENGLISH_NAME = 'Marshallese'


class MAI:
    ISO_639_1 = ''
    ISO_639 = 'mai'
    ENGLISH_NAME = 'Maithili'


class MAK:
    ISO_639_1 = ''
    ISO_639 = 'mak'
    ENGLISH_NAME = 'Makasar'


class MAL:
    ISO_639_1 = ''
    ISO_639 = 'mal'
    ENGLISH_NAME = 'Malayalam'


class MAN:
    ISO_639_1 = ''
    ISO_639 = 'man'
    ENGLISH_NAME = 'Mandingo'


class MAO:
    ISO_639_1 = ''
    ISO_639 = 'mao'
    ENGLISH_NAME = 'Maori'


class MAR:
    ISO_639_1 = 'mr'
    ISO_639 = 'mar'
    ENGLISH_NAME = 'Marathi'


class MAS:
    ISO_639_1 = ''
    ISO_639 = 'mas'
    ENGLISH_NAME = 'Masai'


class MAY:
    ISO_639_1 = ''
    ISO_639 = 'may'
    ENGLISH_NAME = 'Malay'


class MDF:
    ISO_639_1 = ''
    ISO_639 = 'mdf'
    ENGLISH_NAME = 'Moksha'


class MDR:
    ISO_639_1 = ''
    ISO_639 = 'mdr'
    ENGLISH_NAME = 'Mandar'


class MEN:
    ISO_639_1 = ''
    ISO_639 = 'men'
    ENGLISH_NAME = 'Mende'


class MIC:
    ISO_639_1 = ''
    ISO_639 = 'mic'
    ENGLISH_NAME = 'Mikmaq'


class MIN:
    ISO_639_1 = ''
    ISO_639 = 'min'
    ENGLISH_NAME = 'Minangkabau'


class MLG:
    ISO_639_1 = ''
    ISO_639 = 'mlg'
    ENGLISH_NAME = 'Malagasy'


class MLT:
    ISO_639_1 = ''
    ISO_639 = 'mlt'
    ENGLISH_NAME = 'Maltese'


class MNC:
    ISO_639_1 = ''
    ISO_639 = 'mnc'
    ENGLISH_NAME = 'Manchu'


class MNI:
    ISO_639_1 = ''
    ISO_639 = 'mni'
    ENGLISH_NAME = 'Manipuri'


class MOH:
    ISO_639_1 = ''
    ISO_639 = 'moh'
    ENGLISH_NAME = 'Mohawk'


class MON:
    ISO_639_1 = ''
    ISO_639 = 'mon'
    ENGLISH_NAME = 'Mongolian'


class MOS:
    ISO_639_1 = ''
    ISO_639 = 'mos'
    ENGLISH_NAME = 'Mossi'


class MUS:
    ISO_639_1 = ''
    ISO_639 = 'mus'
    ENGLISH_NAME = 'Creek'


class MWL:
    ISO_639_1 = ''
    ISO_639 = 'mwl'
    ENGLISH_NAME = 'Mirandese'


class MWR:
    ISO_639_1 = ''
    ISO_639 = 'mwr'
    ENGLISH_NAME = 'Marwari'


class MYV:
    ISO_639_1 = ''
    ISO_639 = 'myv'
    ENGLISH_NAME = 'Erzya'


class NAP:
    ISO_639_1 = ''
    ISO_639 = 'nap'
    ENGLISH_NAME = 'Neapolitan'


class NAU:
    ISO_639_1 = ''
    ISO_639 = 'nau'
    ENGLISH_NAME = 'Nauru'


class NAV:
    ISO_639_1 = ''
    ISO_639 = 'nav'
    ENGLISH_NAME = 'Navajo'


class NBL:
    ISO_639_1 = ''
    ISO_639 = 'nbl'
    ENGLISH_NAME = 'Ndebele'


class NDE:
    ISO_639_1 = ''
    ISO_639 = 'nde'
    ENGLISH_NAME = 'Ndebele'


class NDO:
    ISO_639_1 = ''
    ISO_639 = 'ndo'
    ENGLISH_NAME = 'Ndonga'


class NEP:
    ISO_639_1 = ''
    ISO_639 = 'nep'
    ENGLISH_NAME = 'Nepali'


class NEW:
    ISO_639_1 = ''
    ISO_639 = 'new'
    ENGLISH_NAME = 'NepalBhasa'


class NIA:
    ISO_639_1 = ''
    ISO_639 = 'nia'
    ENGLISH_NAME = 'Nias'


class NIU:
    ISO_639_1 = ''
    ISO_639 = 'niu'
    ENGLISH_NAME = 'Niuean'


class NNO:
    ISO_639_1 = ''
    ISO_639 = 'nno'
    ENGLISH_NAME = 'NorwegianNynorsk'


class NOB:
    ISO_639_1 = ''
    ISO_639 = 'nob'
    ENGLISH_NAME = 'Bokmål'


class NOG:
    ISO_639_1 = ''
    ISO_639 = 'nog'
    ENGLISH_NAME = 'Nogai'


class NOR:
    ISO_639_1 = ''
    ISO_639 = 'nor'
    ENGLISH_NAME = 'Norwegian'


class NQO:
    ISO_639_1 = ''
    ISO_639 = 'nqo'
    ENGLISH_NAME = 'NKo'


class NSO:
    ISO_639_1 = ''
    ISO_639 = 'nso'
    ENGLISH_NAME = 'Pedi'


class NYA:
    ISO_639_1 = ''
    ISO_639 = 'nya'
    ENGLISH_NAME = 'Chichewa'


class NYM:
    ISO_639_1 = ''
    ISO_639 = 'nym'
    ENGLISH_NAME = 'Nyamwezi'


class NYN:
    ISO_639_1 = ''
    ISO_639 = 'nyn'
    ENGLISH_NAME = 'Nyankole'


class NYO:
    ISO_639_1 = ''
    ISO_639 = 'nyo'
    ENGLISH_NAME = 'Nyoro'


class NZI:
    ISO_639_1 = ''
    ISO_639 = 'nzi'
    ENGLISH_NAME = 'Nzima'


class OJI:
    ISO_639_1 = ''
    ISO_639 = 'oji'
    ENGLISH_NAME = 'Ojibwa'


class ORI:
    ISO_639_1 = 'or'
    ISO_639 = 'ori'
    ENGLISH_NAME = 'Oriya'


class ORM:
    ISO_639_1 = ''
    ISO_639 = 'orm'
    ENGLISH_NAME = 'Oromo'


class OSA:
    ISO_639_1 = ''
    ISO_639 = 'osa'
    ENGLISH_NAME = 'Osage'


class OSS:
    ISO_639_1 = ''
    ISO_639 = 'oss'
    ENGLISH_NAME = 'Ossetian'


class PAG:
    ISO_639_1 = ''
    ISO_639 = 'pag'
    ENGLISH_NAME = 'Pangasinan'


class PAL:
    ISO_639_1 = ''
    ISO_639 = 'pal'
    ENGLISH_NAME = 'Pahlavi'


class PAM:
    ISO_639_1 = ''
    ISO_639 = 'pam'
    ENGLISH_NAME = 'Pampanga'


class PAN:
    ISO_639_1 = ''
    ISO_639 = 'pan'
    ENGLISH_NAME = 'Panjabi'


class PAP:
    ISO_639_1 = ''
    ISO_639 = 'pap'
    ENGLISH_NAME = 'Papiamento'


class PAU:
    ISO_639_1 = ''
    ISO_639 = 'pau'
    ENGLISH_NAME = 'Palauan'


class PER:
    ISO_639_1 = 'fa'
    ISO_639 = 'per'
    ENGLISH_NAME = 'Persian'


class PHN:
    ISO_639_1 = ''
    ISO_639 = 'phn'
    ENGLISH_NAME = 'Phoenician'


class PLI:
    ISO_639_1 = ''
    ISO_639 = 'pli'
    ENGLISH_NAME = 'Pali'


class POL:
    ISO_639_1 = ''
    ISO_639 = 'pol'
    ENGLISH_NAME = 'Polish'


class PON:
    ISO_639_1 = ''
    ISO_639 = 'pon'
    ENGLISH_NAME = 'Pohnpeian'


class POR:
    ISO_639_1 = 'pt'
    ISO_639 = 'por'
    ENGLISH_NAME = 'Portuguese'


class PUS:
    ISO_639_1 = ''
    ISO_639 = 'pus'
    ENGLISH_NAME = 'Pushto'


class QUE:
    ISO_639_1 = ''
    ISO_639 = 'que'
    ENGLISH_NAME = 'Quechua'


class RAJ:
    ISO_639_1 = ''
    ISO_639 = 'raj'
    ENGLISH_NAME = 'Rajasthani'


class RAP:
    ISO_639_1 = ''
    ISO_639 = 'rap'
    ENGLISH_NAME = 'Rapanui'


class RAR:
    ISO_639_1 = ''
    ISO_639 = 'rar'
    ENGLISH_NAME = 'Rarotongan'


class ROH:
    ISO_639_1 = ''
    ISO_639 = 'roh'
    ENGLISH_NAME = 'Romansh'


class ROM:
    ISO_639_1 = ''
    ISO_639 = 'rom'
    ENGLISH_NAME = 'Romany'


class RUM:
    ISO_639_1 = ''
    ISO_639 = 'rum'
    ENGLISH_NAME = 'Romanian'


class RUN:
    ISO_639_1 = ''
    ISO_639 = 'run'
    ENGLISH_NAME = 'Rundi'


class RUP:
    ISO_639_1 = ''
    ISO_639 = 'rup'
    ENGLISH_NAME = 'Aromanian'


class RUS:
    ISO_639_1 = 'ru'
    ISO_639 = 'rus'
    ENGLISH_NAME = 'Russian'


class SAD:
    ISO_639_1 = ''
    ISO_639 = 'sad'
    ENGLISH_NAME = 'Sandawe'


class SAG:
    ISO_639_1 = ''
    ISO_639 = 'sag'
    ENGLISH_NAME = 'Sango'


class SAH:
    ISO_639_1 = ''
    ISO_639 = 'sah'
    ENGLISH_NAME = 'Yakut'


class SAM:
    ISO_639_1 = ''
    ISO_639 = 'sam'
    ENGLISH_NAME = 'SamaritanAramaic'


class SAN:
    ISO_639_1 = ''
    ISO_639 = 'san'
    ENGLISH_NAME = 'Sanskrit'


class SAS:
    ISO_639_1 = ''
    ISO_639 = 'sas'
    ENGLISH_NAME = 'Sasak'


class SAT:
    ISO_639_1 = ''
    ISO_639 = 'sat'
    ENGLISH_NAME = 'Santali'


class SCN:
    ISO_639_1 = ''
    ISO_639 = 'scn'
    ENGLISH_NAME = 'Sicilian'


class SCO:
    ISO_639_1 = ''
    ISO_639 = 'sco'
    ENGLISH_NAME = 'Scots'


class SEL:
    ISO_639_1 = ''
    ISO_639 = 'sel'
    ENGLISH_NAME = 'Selkup'


class SHN:
    ISO_639_1 = ''
    ISO_639 = 'shn'
    ENGLISH_NAME = 'Shan'


class SID:
    ISO_639_1 = ''
    ISO_639 = 'sid'
    ENGLISH_NAME = 'Sidamo'


class SIN:
    ISO_639_1 = ''
    ISO_639 = 'sin'
    ENGLISH_NAME = 'Sinhala'


class SLO:
    ISO_639_1 = ''
    ISO_639 = 'slo'
    ENGLISH_NAME = 'Slovak'


class SLV:
    ISO_639_1 = ''
    ISO_639 = 'slv'
    ENGLISH_NAME = 'Slovenian'


class SMA:
    ISO_639_1 = ''
    ISO_639 = 'sma'
    ENGLISH_NAME = 'SouthernSami'


class SME:
    ISO_639_1 = ''
    ISO_639 = 'sme'
    ENGLISH_NAME = 'NorthernSami'


class SMJ:
    ISO_639_1 = ''
    ISO_639 = 'smj'
    ENGLISH_NAME = 'LuleSami'


class SMN:
    ISO_639_1 = ''
    ISO_639 = 'smn'
    ENGLISH_NAME = 'InariSami'


class SMO:
    ISO_639_1 = ''
    ISO_639 = 'smo'
    ENGLISH_NAME = 'Samoan'


class SMS:
    ISO_639_1 = ''
    ISO_639 = 'sms'
    ENGLISH_NAME = 'SkoltSami'


class SNA:
    ISO_639_1 = ''
    ISO_639 = 'sna'
    ENGLISH_NAME = 'Shona'


class SND:
    ISO_639_1 = ''
    ISO_639 = 'snd'
    ENGLISH_NAME = 'Sindhi'


class SNK:
    ISO_639_1 = ''
    ISO_639 = 'snk'
    ENGLISH_NAME = 'Soninke'


class SOG:
    ISO_639_1 = ''
    ISO_639 = 'sog'
    ENGLISH_NAME = 'Sogdian'


class SOM:
    ISO_639_1 = ''
    ISO_639 = 'som'
    ENGLISH_NAME = 'Somali'


class SOT:
    ISO_639_1 = ''
    ISO_639 = 'sot'
    ENGLISH_NAME = 'Sotho'


class SPA:
    ISO_639_1 = 'es'
    ISO_639 = 'spa'
    ENGLISH_NAME = 'Spanish'


class SRD:
    ISO_639_1 = ''
    ISO_639 = 'srd'
    ENGLISH_NAME = 'Sardinian'


class SRN:
    ISO_639_1 = ''
    ISO_639 = 'srn'
    ENGLISH_NAME = 'SrananTongo'


class SRP:
    ISO_639_1 = ''
    ISO_639 = 'srp'
    ENGLISH_NAME = 'Serbian'


class SRR:
    ISO_639_1 = ''
    ISO_639 = 'srr'
    ENGLISH_NAME = 'Serer'


class SSW:
    ISO_639_1 = ''
    ISO_639 = 'ssw'
    ENGLISH_NAME = 'Swati'


class SUK:
    ISO_639_1 = ''
    ISO_639 = 'suk'
    ENGLISH_NAME = 'Sukuma'


class SUN:
    ISO_639_1 = ''
    ISO_639 = 'sun'
    ENGLISH_NAME = 'Sundanese'


class SUS:
    ISO_639_1 = ''
    ISO_639 = 'sus'
    ENGLISH_NAME = 'Susu'


class SUX:
    ISO_639_1 = ''
    ISO_639 = 'sux'
    ENGLISH_NAME = 'Sumerian'


class SWA:
    ISO_639_1 = ''
    ISO_639 = 'swa'
    ENGLISH_NAME = 'Swahili'


class SWE:
    ISO_639_1 = 'sv'
    ISO_639 = 'swe'
    ENGLISH_NAME = 'Swedish'


class SYC:
    ISO_639_1 = ''
    ISO_639 = 'syc'
    ENGLISH_NAME = 'ClassicalSyriac'


class SYR:
    ISO_639_1 = ''
    ISO_639 = 'syr'
    ENGLISH_NAME = 'Syriac'


class TAH:
    ISO_639_1 = 'th'
    ISO_639 = 'tah'
    ENGLISH_NAME = 'Tahitian'


class TAM:
    ISO_639_1 = ''
    ISO_639 = 'tam'
    ENGLISH_NAME = 'Tamil'


class TAT:
    ISO_639_1 = ''
    ISO_639 = 'tat'
    ENGLISH_NAME = 'Tatar'


class TEL:
    ISO_639_1 = 'te'
    ISO_639 = 'tel'
    ENGLISH_NAME = 'Telugu'


class TEM:
    ISO_639_1 = ''
    ISO_639 = 'tem'
    ENGLISH_NAME = 'Timne'


class TER:
    ISO_639_1 = ''
    ISO_639 = 'ter'
    ENGLISH_NAME = 'Tereno'


class TET:
    ISO_639_1 = ''
    ISO_639 = 'tet'
    ENGLISH_NAME = 'Tetum'


class TGK:
    ISO_639_1 = ''
    ISO_639 = 'tgk'
    ENGLISH_NAME = 'Tajik'


class TGL:
    ISO_639_1 = ''
    ISO_639 = 'tgl'
    ENGLISH_NAME = 'Tagalog'


class THA:
    ISO_639_1 = ''
    ISO_639 = 'tha'
    ENGLISH_NAME = 'Thai'


class TIB:
    ISO_639_1 = ''
    ISO_639 = 'tib'
    ENGLISH_NAME = 'Tibetan'


class TIG:
    ISO_639_1 = ''
    ISO_639 = 'tig'
    ENGLISH_NAME = 'Tigre'


class TIR:
    ISO_639_1 = ''
    ISO_639 = 'tir'
    ENGLISH_NAME = 'Tigrinya'


class TIV:
    ISO_639_1 = ''
    ISO_639 = 'tiv'
    ENGLISH_NAME = 'Tiv'


class TKL:
    ISO_639_1 = ''
    ISO_639 = 'tkl'
    ENGLISH_NAME = 'Tokelau'


class TLH:
    ISO_639_1 = ''
    ISO_639 = 'tlh'
    ENGLISH_NAME = 'Klingon'


class TLI:
    ISO_639_1 = ''
    ISO_639 = 'tli'
    ENGLISH_NAME = 'Tlingit'


class TMH:
    ISO_639_1 = ''
    ISO_639 = 'tmh'
    ENGLISH_NAME = 'Tamashek'


class TOG:
    ISO_639_1 = ''
    ISO_639 = 'tog'
    ENGLISH_NAME = 'Tonga'


class TON:
    ISO_639_1 = ''
    ISO_639 = 'ton'
    ENGLISH_NAME = 'Tonga'


class TPI:
    ISO_639_1 = ''
    ISO_639 = 'tpi'
    ENGLISH_NAME = 'TokPisin'


class TSI:
    ISO_639_1 = ''
    ISO_639 = 'tsi'
    ENGLISH_NAME = 'Tsimshian'


class TSN:
    ISO_639_1 = ''
    ISO_639 = 'tsn'
    ENGLISH_NAME = 'Tswana'


class TSO:
    ISO_639_1 = ''
    ISO_639 = 'tso'
    ENGLISH_NAME = 'Tsonga'


class TUK:
    ISO_639_1 = ''
    ISO_639 = 'tuk'
    ENGLISH_NAME = 'Turkmen'


class TUM:
    ISO_639_1 = ''
    ISO_639 = 'tum'
    ENGLISH_NAME = 'Tumbuka'


class TUR:
    ISO_639_1 = ''
    ISO_639 = 'tur'
    ENGLISH_NAME = 'Turkish'


class TVL:
    ISO_639_1 = ''
    ISO_639 = 'tvl'
    ENGLISH_NAME = 'Tuvalu'


class TWI:
    ISO_639_1 = ''
    ISO_639 = 'twi'
    ENGLISH_NAME = 'Twi'


class TYV:
    ISO_639_1 = ''
    ISO_639 = 'tyv'
    ENGLISH_NAME = 'Tuvinian'


class UDM:
    ISO_639_1 = ''
    ISO_639 = 'udm'
    ENGLISH_NAME = 'Udmurt'


class UGA:
    ISO_639_1 = ''
    ISO_639 = 'uga'
    ENGLISH_NAME = 'Ugaritic'


class UIG:
    ISO_639_1 = ''
    ISO_639 = 'uig'
    ENGLISH_NAME = 'Uighur'


class UKR:
    ISO_639_1 = ''
    ISO_639 = 'ukr'
    ENGLISH_NAME = 'Ukrainian'


class UMB:
    ISO_639_1 = ''
    ISO_639 = 'umb'
    ENGLISH_NAME = 'Umbundu'


class UND:
    ISO_639_1 = ''
    ISO_639 = 'und'
    ENGLISH_NAME = 'Undetermined'


class URD:
    ISO_639_1 = ''
    ISO_639 = 'urd'
    ENGLISH_NAME = 'Urdu'


class UZB:
    ISO_639_1 = ''
    ISO_639 = 'uzb'
    ENGLISH_NAME = 'Uzbek'


class VAI:
    ISO_639_1 = ''
    ISO_639 = 'vai'
    ENGLISH_NAME = 'Vai'


class VEN:
    ISO_639_1 = ''
    ISO_639 = 'ven'
    ENGLISH_NAME = 'Venda'


class VIE:
    ISO_639_1 = ''
    ISO_639 = 'vie'
    ENGLISH_NAME = 'Vietnamese'


class VOL:
    ISO_639_1 = ''
    ISO_639 = 'vol'
    ENGLISH_NAME = 'Volapük'


class VOT:
    ISO_639_1 = ''
    ISO_639 = 'vot'
    ENGLISH_NAME = 'Votic'


class WAL:
    ISO_639_1 = ''
    ISO_639 = 'wal'
    ENGLISH_NAME = 'Wolaitta'


class WAR:
    ISO_639_1 = ''
    ISO_639 = 'war'
    ENGLISH_NAME = 'Waray'


class WAS:
    ISO_639_1 = ''
    ISO_639 = 'was'
    ENGLISH_NAME = 'Washo'


class WEL:
    ISO_639_1 = ''
    ISO_639 = 'wel'
    ENGLISH_NAME = 'Welsh'


class WLN:
    ISO_639_1 = ''
    ISO_639 = 'wln'
    ENGLISH_NAME = 'Walloon'


class WOL:
    ISO_639_1 = ''
    ISO_639 = 'wol'
    ENGLISH_NAME = 'Wolof'


class XAL:
    ISO_639_1 = ''
    ISO_639 = 'xal'
    ENGLISH_NAME = 'Kalmyk'


class XHO:
    ISO_639_1 = ''
    ISO_639 = 'xho'
    ENGLISH_NAME = 'Xhosa'


class YAO:
    ISO_639_1 = ''
    ISO_639 = 'yao'
    ENGLISH_NAME = 'Yao'


class YAP:
    ISO_639_1 = ''
    ISO_639 = 'yap'
    ENGLISH_NAME = 'Yapese'


class YID:
    ISO_639_1 = ''
    ISO_639 = 'yid'
    ENGLISH_NAME = 'Yiddish'


class YOR:
    ISO_639_1 = ''
    ISO_639 = 'yor'
    ENGLISH_NAME = 'Yoruba'


class ZAP:
    ISO_639_1 = ''
    ISO_639 = 'zap'
    ENGLISH_NAME = 'Zapotec'


class ZBL:
    ISO_639_1 = ''
    ISO_639 = 'zbl'
    ENGLISH_NAME = 'Blissymbols'


class ZEN:
    ISO_639_1 = ''
    ISO_639 = 'zen'
    ENGLISH_NAME = 'Zenaga'


class ZGH:
    ISO_639_1 = ''
    ISO_639 = 'zgh'
    ENGLISH_NAME = 'StandardMoroccanTamazight'


class ZHA:
    ISO_639_1 = ''
    ISO_639 = 'zha'
    ENGLISH_NAME = 'Zhuang'


class ZHS:
    ISO_639_1 = ''
    ISO_639 = 'zhs'
    ENGLISH_NAME = 'SimplifiedChinese'


class ZHT:
    ISO_639_1 = ''
    ISO_639 = 'zht'
    ENGLISH_NAME = 'TraditionalChinese'


class ZUL:
    ISO_639_1 = ''
    ISO_639 = 'zul'
    ENGLISH_NAME = 'Zulu'


class ZUN:
    ISO_639_1 = ''
    ISO_639 = 'zun'
    ENGLISH_NAME = 'Zuni'


class ZZA:
    ISO_639_1 = ''
    ISO_639 = 'zza'
    ENGLISH_NAME = 'Zaza'


def get_language_classes():
    import sys
    import inspect

    return inspect.getmembers(sys.modules[__name__], inspect.isclass)



File: chatterbot/__main__.py

import configparser
import sys
import os


def get_chatterbot_version():
    config = configparser.ConfigParser()

    current_directory = os.path.dirname(os.path.abspath(__file__))
    parent_directory = os.path.abspath(os.path.join(current_directory, os.pardir))
    config_file_path = os.path.join(parent_directory, 'setup.cfg')

    config.read(config_file_path)

    return config['chatterbot']['version']


if __name__ == '__main__':
    if '--version' in sys.argv:
        print(get_chatterbot_version())



File: chatterbot/filters.py

def get_recent_repeated_responses(chatbot, conversation, sample=10, threshold=3, quantity=3):
    """
    A filter that eliminates possibly repetitive responses to prevent
    a chat bot from repeating statements that it has recently said.
    """
    from collections import Counter

    # Get the most recent statements from the conversation
    conversation_statements = list(chatbot.storage.filter(
        conversation=conversation,
        order_by=['id']
    ))[sample * -1:]

    text_of_recent_responses = [
        statement.text for statement in conversation_statements
    ]

    counter = Counter(text_of_recent_responses)

    # Find the n most common responses from the conversation
    most_common = counter.most_common(quantity)

    return [
        counted[0] for counted in most_common
        if counted[1] >= threshold
    ]



File: chatterbot/corpus.py

import os
import io
import glob
from pathlib import Path
from chatterbot.exceptions import OptionalDependencyImportError

try:
    from chatterbot_corpus.corpus import DATA_DIRECTORY
except (ImportError, ModuleNotFoundError):
    # Default to the home directory of the current user
    DATA_DIRECTORY = os.path.join(
        Path.home(),
        'chatterbot_corpus',
        'data'
    )


CORPUS_EXTENSION = 'yml'


def get_file_path(dotted_path, extension='json'):
    """
    Reads a dotted file path and returns the file path.
    """
    # If the operating system's file path seperator character is in the string
    if os.sep in dotted_path or '/' in dotted_path:
        # Assume the path is a valid file path
        return dotted_path

    parts = dotted_path.split('.')
    if parts[0] == 'chatterbot':
        parts.pop(0)
        parts[0] = DATA_DIRECTORY

    corpus_path = os.path.join(*parts)

    path_with_extension = '{}.{}'.format(corpus_path, extension)
    if os.path.exists(path_with_extension):
        corpus_path = path_with_extension

    return corpus_path


def read_corpus(file_name):
    """
    Read and return the data from a corpus json file.
    """
    try:
        import yaml
    except ImportError:
        message = (
            'Unable to import "yaml".\n'
            'Please install "pyyaml" to enable chatterbot corpus functionality:\n'
            'pip3 install pyyaml'
        )
        raise OptionalDependencyImportError(message)

    with io.open(file_name, encoding='utf-8') as data_file:
        return yaml.safe_load(data_file)


def list_corpus_files(dotted_path):
    """
    Return a list of file paths to each data file in the specified corpus.
    """
    corpus_path = get_file_path(dotted_path, extension=CORPUS_EXTENSION)
    paths = []

    if os.path.isdir(corpus_path):
        paths = glob.glob(corpus_path + '/**/*.' + CORPUS_EXTENSION, recursive=True)
    else:
        paths.append(corpus_path)

    paths.sort()
    return paths


def load_corpus(*data_file_paths):
    """
    Return the data contained within a specified corpus.
    """
    for file_path in data_file_paths:
        corpus = []
        corpus_data = read_corpus(file_path)

        conversations = corpus_data.get('conversations', [])
        corpus.extend(conversations)

        categories = corpus_data.get('categories', [])

        yield corpus, categories, file_path



File: chatterbot/adapters.py

class Adapter(object):
    """
    A superclass for all adapter classes.

    :param chatbot: A ChatBot instance.
    """

    def __init__(self, chatbot, **kwargs):
        self.chatbot = chatbot

    class AdapterMethodNotImplementedError(NotImplementedError):
        """
        An exception to be raised when an adapter method has not been implemented.
        Typically this indicates that the developer is expected to implement the
        method in a subclass.
        """

        def __init__(self, message='This method must be overridden in a subclass method.'):
            """
            Set the message for the exception.
            """
            super().__init__(message)

    class InvalidAdapterTypeException(Exception):
        """
        An exception to be raised when an adapter
        of an unexpected class type is received.
        """
        pass



File: chatterbot/ext/__init__.py




File: chatterbot/ext/django_chatterbot/model_admin.py

from django.contrib import admin


class StatementAdmin(admin.ModelAdmin):
    list_display = ('text', 'in_response_to', 'conversation', 'created_at', )
    list_filter = ('text', 'created_at', )
    search_fields = ('text', )


class TagAdmin(admin.ModelAdmin):
    list_display = ('name', )
    list_filter = ('name', )
    search_fields = ('name', )



File: chatterbot/ext/django_chatterbot/abstract_models.py

from chatterbot.conversation import StatementMixin
from chatterbot import constants
from django.db import models
from django.utils import timezone
from django.conf import settings


DJANGO_APP_NAME = constants.DEFAULT_DJANGO_APP_NAME
STATEMENT_MODEL = 'Statement'
TAG_MODEL = 'Tag'

if hasattr(settings, 'CHATTERBOT'):
    """
    Allow related models to be overridden in the project settings.
    Default to the original settings if one is not defined.
    """
    DJANGO_APP_NAME = settings.CHATTERBOT.get(
        'django_app_name',
        DJANGO_APP_NAME
    )
    STATEMENT_MODEL = settings.CHATTERBOT.get(
        'statement_model',
        STATEMENT_MODEL
    )


class AbstractBaseTag(models.Model):
    """
    The abstract base tag allows other models to be created
    using the attributes that exist on the default models.
    """

    name = models.SlugField(
        max_length=constants.TAG_NAME_MAX_LENGTH,
        unique=True
    )

    class Meta:
        abstract = True

    def __str__(self):
        return self.name


class AbstractBaseStatement(models.Model, StatementMixin):
    """
    The abstract base statement allows other models to be created
    using the attributes that exist on the default models.
    """

    text = models.CharField(
        max_length=constants.STATEMENT_TEXT_MAX_LENGTH
    )

    search_text = models.CharField(
        max_length=constants.STATEMENT_TEXT_MAX_LENGTH,
        blank=True
    )

    conversation = models.CharField(
        max_length=constants.CONVERSATION_LABEL_MAX_LENGTH
    )

    created_at = models.DateTimeField(
        default=timezone.now,
        help_text='The date and time that the statement was created at.'
    )

    in_response_to = models.CharField(
        max_length=constants.STATEMENT_TEXT_MAX_LENGTH,
        null=True
    )

    search_in_response_to = models.CharField(
        max_length=constants.STATEMENT_TEXT_MAX_LENGTH,
        blank=True
    )

    persona = models.CharField(
        max_length=constants.PERSONA_MAX_LENGTH
    )

    tags = models.ManyToManyField(
        TAG_MODEL,
        related_name='statements'
    )

    # This is the confidence with which the chat bot believes
    # this is an accurate response. This value is set when the
    # statement is returned by the chat bot.
    confidence = 0

    class Meta:
        abstract = True

    def __str__(self):
        if len(self.text.strip()) > 60:
            return '{}...'.format(self.text[:57])
        elif len(self.text.strip()) > 0:
            return self.text
        return '<empty>'

    def get_tags(self):
        """
        Return the list of tags for this statement.
        (Overrides the method from StatementMixin)
        """
        return list(self.tags.values_list('name', flat=True))

    def add_tags(self, *tags):
        """
        Add a list of strings to the statement as tags.
        (Overrides the method from StatementMixin)
        """
        for _tag in tags:
            self.tags.get_or_create(name=_tag)



File: chatterbot/ext/django_chatterbot/models.py

from chatterbot.ext.django_chatterbot.abstract_models import AbstractBaseStatement, AbstractBaseTag


class Statement(AbstractBaseStatement):
    """
    A statement represents a single spoken entity, sentence or
    phrase that someone can say.
    """
    pass


class Tag(AbstractBaseTag):
    """
    A label that categorizes a statement.
    """
    pass



File: chatterbot/ext/django_chatterbot/__init__.py

default_app_config = (
    'chatterbot.ext.django_chatterbot.apps.DjangoChatterBotConfig'
)



File: chatterbot/ext/django_chatterbot/apps.py

from django.apps import AppConfig


class DjangoChatterBotConfig(AppConfig):

    name = 'chatterbot.ext.django_chatterbot'
    label = 'django_chatterbot'
    verbose_name = 'Django ChatterBot'



File: chatterbot/ext/django_chatterbot/admin.py

from django.contrib import admin
from chatterbot.ext.django_chatterbot.model_admin import StatementAdmin, TagAdmin
from chatterbot.ext.django_chatterbot.models import Statement, Tag


admin.site.register(Statement, StatementAdmin)
admin.site.register(Tag, TagAdmin)



File: chatterbot/ext/django_chatterbot/settings.py

"""
Default ChatterBot settings for Django.
"""
from django.conf import settings
from chatterbot import constants


CHATTERBOT_SETTINGS = getattr(settings, 'CHATTERBOT', {})

CHATTERBOT_DEFAULTS = {
    'name': 'ChatterBot',
    'storage_adapter': 'chatterbot.storage.DjangoStorageAdapter',
    'django_app_name': constants.DEFAULT_DJANGO_APP_NAME
}

CHATTERBOT = CHATTERBOT_DEFAULTS.copy()
CHATTERBOT.update(CHATTERBOT_SETTINGS)



File: chatterbot/ext/django_chatterbot/migrations/0013_change_conversations.py

# Generated by Django 1.11 on 2018-09-13 01:01
from django.db import migrations, models


class Migration(migrations.Migration):

    dependencies = [
        ('django_chatterbot', '0012_statement_created_at'),
    ]

    operations = [
        migrations.RemoveField(
            model_name='conversation',
            name='responses',
        ),
        migrations.RemoveField(
            model_name='response',
            name='response',
        ),
        migrations.RemoveField(
            model_name='response',
            name='statement',
        ),
        migrations.AddField(
            model_name='statement',
            name='conversation',
            field=models.CharField(default='default', max_length=32),
            preserve_default=False,
        ),
        migrations.AddField(
            model_name='statement',
            name='in_response_to',
            field=models.CharField(max_length=400, null=True),
        ),
        migrations.AlterField(
            model_name='statement',
            name='text',
            field=models.CharField(max_length=400),
        ),
        migrations.DeleteModel(
            name='Conversation',
        ),
        migrations.DeleteModel(
            name='Response',
        ),
    ]



File: chatterbot/ext/django_chatterbot/migrations/0016_statement_stemmed_text.py

from django.db import migrations, models


class Migration(migrations.Migration):

    dependencies = [
        ('django_chatterbot', '0015_statement_persona'),
    ]

    operations = [
        migrations.AddField(
            model_name='statement',
            name='search_text',
            field=models.CharField(blank=True, max_length=400),
        ),
        migrations.AddField(
            model_name='statement',
            name='search_in_response_to',
            field=models.CharField(blank=True, max_length=400),
        ),
    ]



File: chatterbot/ext/django_chatterbot/migrations/0018_text_max_length.py

from django.db import migrations, models


class Migration(migrations.Migration):

    dependencies = [
        ('django_chatterbot', '0017_tags_unique'),
    ]

    operations = [
        migrations.AlterField(
            model_name='statement',
            name='in_response_to',
            field=models.CharField(max_length=255, null=True),
        ),
        migrations.AlterField(
            model_name='statement',
            name='search_in_response_to',
            field=models.CharField(blank=True, max_length=255),
        ),
        migrations.AlterField(
            model_name='statement',
            name='search_text',
            field=models.CharField(blank=True, max_length=255),
        ),
        migrations.AlterField(
            model_name='statement',
            name='text',
            field=models.CharField(max_length=255),
        ),
    ]



File: chatterbot/ext/django_chatterbot/migrations/0017_tags_unique.py

from django.db import migrations, models


class Migration(migrations.Migration):

    dependencies = [
        ('django_chatterbot', '0016_statement_stemmed_text'),
    ]

    operations = [
        migrations.RemoveField(
            model_name='tag',
            name='statements',
        ),
        migrations.AddField(
            model_name='statement',
            name='tags',
            field=models.ManyToManyField(
                related_name='statements',
                to='django_chatterbot.Tag'
            ),
        ),
        migrations.AlterField(
            model_name='tag',
            name='name',
            field=models.SlugField(unique=True),
        ),
    ]



File: chatterbot/ext/django_chatterbot/migrations/0004_rename_in_response_to.py

# Generated by Django 1.10.3 on 2016-12-04 23:52
from django.db import migrations, models
import django.db.models.deletion


class Migration(migrations.Migration):

    dependencies = [
        ('django_chatterbot', '0003_change_occurrence_default'),
    ]

    operations = [
        migrations.AlterField(
            model_name='response',
            name='statement',
            field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='in_response', to='django_chatterbot.Statement'),
        ),
        migrations.AlterField(
            model_name='response',
            name='response',
            field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='responses', to='django_chatterbot.Statement'),
        ),
    ]



File: chatterbot/ext/django_chatterbot/migrations/0007_response_created_at.py

# Generated by Django 1.11 on 2017-07-18 00:16
from django.db import migrations, models
import django.utils.timezone


class Migration(migrations.Migration):

    dependencies = [
        ('django_chatterbot', '0006_create_conversation'),
    ]

    operations = [
        migrations.AddField(
            model_name='response',
            name='created_at',
            field=models.DateTimeField(
                default=django.utils.timezone.now,
                help_text='The date and time that this response was created at.'
            ),
        ),
    ]



File: chatterbot/ext/django_chatterbot/migrations/0008_update_conversations.py

# Generated by Django 1.11 on 2017-07-18 11:25
from django.db import migrations, models


class Migration(migrations.Migration):

    dependencies = [
        ('django_chatterbot', '0007_response_created_at'),
    ]

    operations = [
        migrations.RemoveField(
            model_name='conversation',
            name='statements',
        ),
        migrations.RemoveField(
            model_name='response',
            name='occurrence',
        ),
        migrations.RemoveField(
            model_name='statement',
            name='created_at',
        ),
        migrations.AddField(
            model_name='conversation',
            name='responses',
            field=models.ManyToManyField(help_text='The responses in this conversation.', related_name='conversations', to='django_chatterbot.Response'),
        ),
    ]



File: chatterbot/ext/django_chatterbot/migrations/0010_statement_text.py

# Generated by Django 1.11.4 on 2017-08-16 00:56
from django.db import migrations, models


class Migration(migrations.Migration):

    dependencies = [
        ('django_chatterbot', '0009_tags'),
    ]

    operations = [
        migrations.AlterField(
            model_name='statement',
            name='text',
            field=models.CharField(max_length=400, unique=True),
        ),
    ]



File: chatterbot/ext/django_chatterbot/migrations/0012_statement_created_at.py

from django.db import migrations, models
import django.utils.timezone


class Migration(migrations.Migration):

    dependencies = [
        ('django_chatterbot', '0011_blank_extra_data'),
    ]

    operations = [
        migrations.AddField(
            model_name='statement',
            name='created_at',
            field=models.DateTimeField(
                default=django.utils.timezone.now,
                help_text='The date and time that the statement was created at.'
            ),
        ),
    ]



File: chatterbot/ext/django_chatterbot/migrations/__init__.py




File: chatterbot/ext/django_chatterbot/migrations/0005_statement_created_at.py

# Generated by Django 1.10.1 on 2016-12-29 19:20
from django.db import migrations, models
import django.utils.timezone


class Migration(migrations.Migration):

    dependencies = [
        ('django_chatterbot', '0004_rename_in_response_to'),
    ]

    operations = [
        migrations.AddField(
            model_name='statement',
            name='created_at',
            field=models.DateTimeField(
                default=django.utils.timezone.now,
                help_text='The date and time that this statement was created at.'
            ),
        ),
    ]



File: chatterbot/ext/django_chatterbot/migrations/0006_create_conversation.py

# Generated by Django 1.9 on 2017-01-17 07:02
from django.db import migrations, models
import django.db.models.deletion
import django.utils.timezone


class Migration(migrations.Migration):

    dependencies = [
        ('django_chatterbot', '0005_statement_created_at'),
    ]

    operations = [
        migrations.CreateModel(
            name='Conversation',
            fields=[
                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
            ],
        ),
        migrations.AlterField(
            model_name='statement',
            name='created_at',
            field=models.DateTimeField(default=django.utils.timezone.now, help_text='The date and time that this statement was created at.'),
        ),
        migrations.AddField(
            model_name='conversation',
            name='statements',
            field=models.ManyToManyField(help_text='The statements in this conversation.', related_name='conversation', to='django_chatterbot.Statement'),
        ),
    ]



File: chatterbot/ext/django_chatterbot/migrations/0002_statement_extra_data.py

# Generated by Django 1.10.2 on 2016-10-30 12:13
from django.db import migrations, models


class Migration(migrations.Migration):

    dependencies = [
        ('django_chatterbot', '0001_initial'),
    ]

    operations = [
        migrations.AddField(
            model_name='statement',
            name='extra_data',
            field=models.CharField(default='{}', max_length=500),
            preserve_default=False,
        ),
    ]



File: chatterbot/ext/django_chatterbot/migrations/0011_blank_extra_data.py

# Generated by Django 1.11.4 on 2017-08-20 13:55
from django.db import migrations, models


class Migration(migrations.Migration):

    dependencies = [
        ('django_chatterbot', '0010_statement_text'),
    ]

    operations = [
        migrations.AlterField(
            model_name='statement',
            name='extra_data',
            field=models.CharField(blank=True, max_length=500),
        ),
    ]



File: chatterbot/ext/django_chatterbot/migrations/0009_tags.py

# Generated by Django 1.11a1 on 2017-07-07 00:12
from django.db import migrations, models


class Migration(migrations.Migration):

    dependencies = [
        ('django_chatterbot', '0008_update_conversations'),
    ]

    operations = [
        migrations.CreateModel(
            name='Tag',
            fields=[
                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
                ('name', models.SlugField()),
            ],
            options={
                'abstract': False,
            },
        ),
        migrations.AlterField(
            model_name='statement',
            name='text',
            field=models.CharField(max_length=255, unique=True),
        ),
        migrations.AddField(
            model_name='tag',
            name='statements',
            field=models.ManyToManyField(related_name='tags', to='django_chatterbot.Statement'),
        ),
    ]



File: chatterbot/ext/django_chatterbot/migrations/0014_remove_statement_extra_data.py

from django.db import migrations


class Migration(migrations.Migration):

    dependencies = [
        ('django_chatterbot', '0013_change_conversations'),
    ]

    operations = [
        migrations.RemoveField(
            model_name='statement',
            name='extra_data',
        ),
    ]



File: chatterbot/ext/django_chatterbot/migrations/0003_change_occurrence_default.py

# Generated by Django 1.9 on 2016-12-12 00:06
from django.db import migrations, models


class Migration(migrations.Migration):

    dependencies = [
        ('django_chatterbot', '0002_statement_extra_data'),
    ]

    operations = [
        migrations.AlterField(
            model_name='response',
            name='occurrence',
            field=models.PositiveIntegerField(default=1),
        ),
    ]



File: chatterbot/ext/django_chatterbot/migrations/0001_initial.py

from django.db import migrations, models
import django.db.models.deletion


class Migration(migrations.Migration):

    initial = True

    dependencies = []

    operations = [
        migrations.CreateModel(
            name='Response',
            fields=[
                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
                ('occurrence', models.PositiveIntegerField(default=0)),
            ],
        ),
        migrations.CreateModel(
            name='Statement',
            fields=[
                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
                ('text', models.CharField(max_length=255, unique=True)),
            ],
        ),
        migrations.AddField(
            model_name='response',
            name='response',
            field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='+', to='django_chatterbot.Statement'),
        ),
        migrations.AddField(
            model_name='response',
            name='statement',
            field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='in_response_to', to='django_chatterbot.Statement'),
        ),
    ]



File: chatterbot/ext/django_chatterbot/migrations/0015_statement_persona.py

from django.db import migrations, models


class Migration(migrations.Migration):

    dependencies = [
        ('django_chatterbot', '0014_remove_statement_extra_data'),
    ]

    operations = [
        migrations.AddField(
            model_name='statement',
            name='persona',
            field=models.CharField(default='', max_length=50),
            preserve_default=False,
        ),
    ]



File: chatterbot/ext/sqlalchemy_app/models.py

from sqlalchemy import Table, Column, Integer, String, DateTime, ForeignKey
from sqlalchemy.orm import relationship
from sqlalchemy.sql import func
from sqlalchemy.ext.declarative import declared_attr, declarative_base

from chatterbot.conversation import StatementMixin
from chatterbot import constants


class ModelBase(object):
    """
    An augmented base class for SqlAlchemy models.
    """

    @declared_attr
    def __tablename__(cls):
        """
        Return the lowercase class name as the name of the table.
        """
        return cls.__name__.lower()

    id = Column(
        Integer,
        primary_key=True,
        autoincrement=True
    )


Base = declarative_base(cls=ModelBase)


tag_association_table = Table(
    'tag_association',
    Base.metadata,
    Column('tag_id', Integer, ForeignKey('tag.id')),
    Column('statement_id', Integer, ForeignKey('statement.id'))
)


class Tag(Base):
    """
    A tag that describes a statement.
    """

    name = Column(
        String(constants.TAG_NAME_MAX_LENGTH),
        unique=True
    )


class Statement(Base, StatementMixin):
    """
    A Statement represents a sentence or phrase.
    """

    confidence = 0

    text = Column(
        String(constants.STATEMENT_TEXT_MAX_LENGTH)
    )

    search_text = Column(
        String(constants.STATEMENT_TEXT_MAX_LENGTH),
        nullable=False,
        server_default=''
    )

    conversation = Column(
        String(constants.CONVERSATION_LABEL_MAX_LENGTH),
        nullable=False,
        server_default=''
    )

    created_at = Column(
        DateTime(timezone=True),
        server_default=func.now()
    )

    tags = relationship(
        'Tag',
        secondary=lambda: tag_association_table,
        backref='statements'
    )

    in_response_to = Column(
        String(constants.STATEMENT_TEXT_MAX_LENGTH),
        nullable=True
    )

    search_in_response_to = Column(
        String(constants.STATEMENT_TEXT_MAX_LENGTH),
        nullable=False,
        server_default=''
    )

    persona = Column(
        String(constants.PERSONA_MAX_LENGTH),
        nullable=False,
        server_default=''
    )

    def get_tags(self):
        """
        Return a list of tags for this statement.
        """
        return [tag.name for tag in self.tags]

    def add_tags(self, *tags):
        """
        Add a list of strings to the statement as tags.
        """
        self.tags.extend([
            Tag(name=tag) for tag in tags
        ])



File: chatterbot/ext/sqlalchemy_app/__init__.py




File: chatterbot/logic/logic_adapter.py

from chatterbot.adapters import Adapter
from chatterbot.storage import StorageAdapter
from chatterbot.search import IndexedTextSearch
from chatterbot.conversation import Statement


class LogicAdapter(Adapter):
    """
    This is an abstract class that represents the interface
    that all logic adapters should implement.

    :param search_algorithm_name: The name of the search algorithm that should
        be used to search for close matches to the provided input.
        Defaults to the value of ``Search.name``.

    :param maximum_similarity_threshold:
        The maximum amount of similarity between two statement that is required
        before the search process is halted. The search for a matching statement
        will continue until a statement with a greater than or equal similarity
        is found or the search set is exhausted.
        Defaults to 0.95

    :param response_selection_method:
          The a response selection method.
          Defaults to ``get_first_response``
    :type response_selection_method: collections.abc.Callable

    :param default_response:
          The default response returned by this logic adaper
          if there is no other possible response to return.
    :type default_response: str or list or tuple
    """

    def __init__(self, chatbot, **kwargs):
        super().__init__(chatbot, **kwargs)
        from chatterbot.response_selection import get_first_response

        self.search_algorithm_name = kwargs.get(
            'search_algorithm_name',
            IndexedTextSearch.name
        )

        self.search_algorithm = self.chatbot.search_algorithms[
            self.search_algorithm_name
        ]

        self.maximum_similarity_threshold = kwargs.get(
            'maximum_similarity_threshold', 0.95
        )

        # By default, select the first available response
        self.select_response = kwargs.get(
            'response_selection_method',
            get_first_response
        )

        default_responses = kwargs.get('default_response', [])

        # Convert a single string into a list
        if isinstance(default_responses, str):
            default_responses = [
                default_responses
            ]

        self.default_responses = [
            Statement(text=default) for default in default_responses
        ]

    def can_process(self, statement):
        """
        A preliminary check that is called to determine if a
        logic adapter can process a given statement. By default,
        this method returns true but it can be overridden in
        child classes as needed.

        :rtype: bool
        """
        return True

    def process(self, statement, additional_response_selection_parameters=None):
        """
        Override this method and implement your logic for selecting a response to an input statement.

        A confidence value and the selected response statement should be returned.
        The confidence value represents a rating of how accurate the logic adapter
        expects the selected response to be. Confidence scores are used to select
        the best response from multiple logic adapters.

        The confidence value should be a number between 0 and 1 where 0 is the
        lowest confidence level and 1 is the highest.

        :param statement: An input statement to be processed by the logic adapter.
        :type statement: Statement

        :param additional_response_selection_parameters: Parameters to be used when
            filtering results to choose a response from.
        :type additional_response_selection_parameters: dict

        :rtype: Statement
        """
        raise self.AdapterMethodNotImplementedError()

    def get_default_response(self, input_statement):
        """
        This method is called when a logic adapter is unable to generate any
        other meaningful response.
        """
        from random import choice

        if self.default_responses:
            response = choice(self.default_responses)
        else:
            try:
                response = self.chatbot.storage.get_random()
            except StorageAdapter.EmptyDatabaseException:
                response = input_statement

        self.chatbot.logger.info(
            'No known response to the input was found. Selecting a random response.'
        )

        # Set confidence to zero because a random response is selected
        response.confidence = 0

        return response

    @property
    def class_name(self):
        """
        Return the name of the current logic adapter class.
        This is typically used for logging and debugging.
        """
        return str(self.__class__.__name__)



File: chatterbot/logic/__init__.py

from chatterbot.logic.logic_adapter import LogicAdapter
from chatterbot.logic.best_match import BestMatch
from chatterbot.logic.mathematical_evaluation import MathematicalEvaluation
from chatterbot.logic.specific_response import SpecificResponseAdapter
from chatterbot.logic.time_adapter import TimeLogicAdapter
from chatterbot.logic.unit_conversion import UnitConversion

__all__ = (
    'LogicAdapter',
    'BestMatch',
    'MathematicalEvaluation',
    'SpecificResponseAdapter',
    'TimeLogicAdapter',
    'UnitConversion',
)



File: chatterbot/logic/best_match.py

from chatterbot.logic import LogicAdapter
from chatterbot import filters


class BestMatch(LogicAdapter):
    """
    A logic adapter that returns a response based on known responses to
    the closest matches to the input statement.

    :param excluded_words:
        The excluded_words parameter allows a list of words to be set that will
        prevent the logic adapter from returning statements that have text
        containing any of those words. This can be useful for preventing your
        chat bot from saying swears when it is being demonstrated in front of
        an audience.
        Defaults to None
    :type excluded_words: list
    """

    def __init__(self, chatbot, **kwargs):
        super().__init__(chatbot, **kwargs)

        self.excluded_words = kwargs.get('excluded_words')

    def process(self, input_statement, additional_response_selection_parameters=None):
        search_results = self.search_algorithm.search(input_statement)

        # Use the input statement as the closest match if no other results are found
        closest_match = next(search_results, input_statement)

        # Search for the closest match to the input statement
        for result in search_results:
            closest_match = result

            # Stop searching if a match that is close enough is found
            if result.confidence >= self.maximum_similarity_threshold:
                break

        self.chatbot.logger.info('Using "{}" as a close match to "{}" with a confidence of {}'.format(
            closest_match.text, input_statement.text, closest_match.confidence
        ))

        recent_repeated_responses = filters.get_recent_repeated_responses(
            self.chatbot,
            input_statement.conversation
        )

        for index, recent_repeated_response in enumerate(recent_repeated_responses):
            self.chatbot.logger.info('{}. Excluding recent repeated response of "{}"'.format(
                index, recent_repeated_response
            ))

        response_selection_parameters = {
            'search_in_response_to': closest_match.search_text,
            'exclude_text': recent_repeated_responses,
            'exclude_text_words': self.excluded_words
        }

        alternate_response_selection_parameters = {
            'search_in_response_to': self.chatbot.storage.tagger.get_text_index_string(
                input_statement.text
            ),
            'exclude_text': recent_repeated_responses,
            'exclude_text_words': self.excluded_words
        }

        if additional_response_selection_parameters:
            response_selection_parameters.update(additional_response_selection_parameters)
            alternate_response_selection_parameters.update(additional_response_selection_parameters)

        # Get all statements that are in response to the closest match
        response_list = list(self.chatbot.storage.filter(**response_selection_parameters))

        alternate_response_list = []

        if not response_list:
            self.chatbot.logger.info('No responses found. Generating alternate response list.')
            alternate_response_list = list(self.chatbot.storage.filter(**alternate_response_selection_parameters))

        if response_list:
            self.chatbot.logger.info(
                'Selecting response from {} optimal responses.'.format(
                    len(response_list)
                )
            )

            response = self.select_response(
                input_statement,
                response_list,
                self.chatbot.storage
            )

            response.confidence = closest_match.confidence
            self.chatbot.logger.info('Response selected. Using "{}"'.format(response.text))
        elif alternate_response_list:
            '''
            The case where there was no responses returned for the selected match
            but a value exists for the statement the match is in response to.
            '''
            self.chatbot.logger.info(
                'Selecting response from {} optimal alternate responses.'.format(
                    len(alternate_response_list)
                )
            )
            response = self.select_response(
                input_statement,
                alternate_response_list,
                self.chatbot.storage
            )

            response.confidence = closest_match.confidence
            self.chatbot.logger.info('Alternate response selected. Using "{}"'.format(response.text))
        else:
            response = self.get_default_response(input_statement)

        return response



File: chatterbot/logic/mathematical_evaluation.py

from chatterbot.logic import LogicAdapter
from chatterbot.conversation import Statement
from chatterbot import languages


class MathematicalEvaluation(LogicAdapter):
    """
    The MathematicalEvaluation logic adapter parses input to determine
    whether the user is asking a question that requires math to be done.
    If so, the equation is extracted from the input and returned with
    the evaluated result.

    For example:
        User: 'What is three plus five?'
        Bot: 'Three plus five equals eight'

    :kwargs:
        * *language* (``object``) --
          The language is set to ``chatterbot.languages.ENG`` for English by default.
    """

    def __init__(self, chatbot, **kwargs):
        super().__init__(chatbot, **kwargs)

        self.language = kwargs.get('language', languages.ENG)
        self.cache = {}

    def can_process(self, statement):
        """
        Determines whether it is appropriate for this
        adapter to respond to the user input.
        """
        response = self.process(statement)
        self.cache[statement.text] = response
        return response.confidence == 1

    def process(self, statement, additional_response_selection_parameters=None):
        """
        Takes a statement string.
        Returns the equation from the statement with the mathematical terms solved.
        """
        from mathparse import mathparse

        input_text = statement.text

        # Use the result cached by the process method if it exists
        if input_text in self.cache:
            cached_result = self.cache[input_text]
            self.cache = {}
            return cached_result

        # Getting the mathematical terms within the input statement
        expression = mathparse.extract_expression(input_text, language=self.language.ISO_639.upper())

        response = Statement(text=expression)

        try:
            response.text = '{} = {}'.format(
                response.text,
                mathparse.parse(expression, language=self.language.ISO_639.upper())
            )

            # The confidence is 1 if the expression could be evaluated
            response.confidence = 1
        except mathparse.PostfixTokenEvaluationException:
            response.confidence = 0

        return response



File: chatterbot/logic/unit_conversion.py

from chatterbot.logic import LogicAdapter
from chatterbot.conversation import Statement
from chatterbot.exceptions import OptionalDependencyImportError
from chatterbot import languages
from chatterbot import parsing
from mathparse import mathparse
import re


class UnitConversion(LogicAdapter):
    """
    The UnitConversion logic adapter parse inputs to convert values
    between several metric units.

    For example:
        User: 'How many meters are in one kilometer?'
        Bot: '1000.0'

    :kwargs:
        * *language* (``object``) --
        The language is set to ``chatterbot.languages.ENG`` for English by default.
    """

    def __init__(self, chatbot, **kwargs):
        super().__init__(chatbot, **kwargs)
        try:
            from pint import UnitRegistry
        except ImportError:
            message = (
                'Unable to import "pint".\n'
                'Please install "pint" before using the UnitConversion logic adapter:\n'
                'pip3 install pint'
            )
            raise OptionalDependencyImportError(message)

        self.language = kwargs.get('language', languages.ENG)
        self.cache = {}
        self.patterns = [
            (
                re.compile(r'''
                   (([Hh]ow\s+many)\s+
                   (?P<target>\S+)\s+ # meter, celsius, hours
                   ((are)*\s*in)\s+
                   (?P<number>([+-]?\d+(?:\.\d+)?)|(a|an)|(%s[-\s]?)+)\s+
                   (?P<from>\S+)\s*) # meter, celsius, hours
                   ''' % (parsing.numbers),
                    (re.VERBOSE | re.IGNORECASE)
                ),
                lambda m: self.handle_matches(m)
            ),
            (
                re.compile(r'''
                   ((?P<number>([+-]?\d+(?:\.\d+)?)|(%s[-\s]?)+)\s+
                   (?P<from>\S+)\s+ # meter, celsius, hours
                   (to)\s+
                   (?P<target>\S+)\s*) # meter, celsius, hours
                   ''' % (parsing.numbers),
                    (re.VERBOSE | re.IGNORECASE)
                ),
                lambda m: self.handle_matches(m)
            ),
            (
                re.compile(r'''
                   ((?P<number>([+-]?\d+(?:\.\d+)?)|(a|an)|(%s[-\s]?)+)\s+
                   (?P<from>\S+)\s+ # meter, celsius, hours
                   (is|are)\s+
                   (how\s+many)*\s+
                   (?P<target>\S+)\s*) # meter, celsius, hours
                   ''' % (parsing.numbers),
                    (re.VERBOSE | re.IGNORECASE)
                ),
                lambda m: self.handle_matches(m)
            )
        ]
        self.unit_registry = UnitRegistry()

    def get_unit(self, unit_variations):
        """
        Get the first match unit metric object supported by pint library
        given a variation of unit metric names (Ex:['HOUR', 'hour']).

        :param unit_variations: A list of strings with names of units
        :type unit_variations: str
        """
        for unit in unit_variations:
            try:
                return getattr(self.unit_registry, unit)
            except Exception:
                continue
        return None

    def get_valid_units(self, from_unit, target_unit):
        """
        Returns the first match `pint.unit.Unit` object for from_unit and
        target_unit strings from a possible variation of metric unit names
        supported by pint library.

        :param from_unit: source metric unit
        :type from_unit: str

        :param from_unit: target metric unit
        :type from_unit: str
        """
        from_unit_variations = [from_unit.lower(), from_unit.upper()]
        target_unit_variations = [target_unit.lower(), target_unit.upper()]
        from_unit = self.get_unit(from_unit_variations)
        target_unit = self.get_unit(target_unit_variations)
        return from_unit, target_unit

    def handle_matches(self, match):
        """
        Returns a response statement from a matched input statement.

        :param match: It is a valid matched pattern from the input statement
        :type: `_sre.SRE_Match`
        """
        response = Statement(text='')

        from_parsed = match.group("from")
        target_parsed = match.group("target")
        n_statement = match.group("number")

        if n_statement == 'a' or n_statement == 'an':
            n_statement = '1.0'

        n = mathparse.parse(n_statement, self.language.ISO_639.upper())

        from_parsed, target_parsed = self.get_valid_units(from_parsed, target_parsed)

        if from_parsed is None or target_parsed is None:
            response.confidence = 0.0
        else:
            from_value = self.unit_registry.Quantity(float(n), from_parsed)
            target_value = from_value.to(target_parsed)
            response.confidence = 1.0
            response.text = str(target_value.magnitude)

        return response

    def can_process(self, statement):
        response = self.process(statement)
        self.cache[statement.text] = response
        return response.confidence == 1.0

    def process(self, statement, additional_response_selection_parameters=None):
        response = Statement(text='')
        input_text = statement.text
        try:
            # Use the result cached by the process method if it exists
            if input_text in self.cache:
                response = self.cache[input_text]
                self.cache = {}
                return response

            for pattern, func in self.patterns:
                p = pattern.match(input_text)
                if p is not None:
                    response = func(p)
                    if response.confidence == 1.0:
                        break
        except Exception:
            response.confidence = 0.0
        finally:
            return response



File: chatterbot/logic/specific_response.py

from chatterbot.logic import LogicAdapter


class SpecificResponseAdapter(LogicAdapter):
    """
    Return a specific response to a specific input.

    :kwargs:
        * *input_text* (``str``) --
          The input text that triggers this logic adapter.
        * *output_text* (``str``) --
          The output text returned by this logic adapter.
    """

    def __init__(self, chatbot, **kwargs):
        super().__init__(chatbot, **kwargs)
        from chatterbot.conversation import Statement

        self.input_text = kwargs.get('input_text')

        output_text = kwargs.get('output_text')
        self.response_statement = Statement(text=output_text)

    def can_process(self, statement):
        if statement.text == self.input_text:
            return True

        return False

    def process(self, statement, additional_response_selection_parameters=None):

        if statement.text == self.input_text:
            self.response_statement.confidence = 1
        else:
            self.response_statement.confidence = 0

        return self.response_statement



File: chatterbot/logic/time_adapter.py

from datetime import datetime
from chatterbot.logic import LogicAdapter
from chatterbot.conversation import Statement
from chatterbot.exceptions import OptionalDependencyImportError


class TimeLogicAdapter(LogicAdapter):
    """
    The TimeLogicAdapter returns the current time.

    :kwargs:
        * *positive* (``list``) --
          The time-related questions used to identify time questions.
          Defaults to a list of English sentences.
        * *negative* (``list``) --
          The non-time-related questions used to identify time questions.
          Defaults to a list of English sentences.
    """

    def __init__(self, chatbot, **kwargs):
        super().__init__(chatbot, **kwargs)
        try:
            from nltk import NaiveBayesClassifier
        except ImportError:
            message = (
                'Unable to import "nltk".\n'
                'Please install "nltk" before using the TimeLogicAdapter:\n'
                'pip3 install nltk'
            )
            raise OptionalDependencyImportError(message)

        self.positive = kwargs.get('positive', [
            'what time is it',
            'hey what time is it',
            'do you have the time',
            'do you know the time',
            'do you know what time it is',
            'what is the time'
        ])

        self.negative = kwargs.get('negative', [
            'it is time to go to sleep',
            'what is your favorite color',
            'i had a great time',
            'thyme is my favorite herb',
            'do you have time to look at my essay',
            'how do you have the time to do all this'
            'what is it'
        ])

        labeled_data = (
            [
                (name, 0) for name in self.negative
            ] + [
                (name, 1) for name in self.positive
            ]
        )

        train_set = [
            (self.time_question_features(text), n) for (text, n) in labeled_data
        ]

        self.classifier = NaiveBayesClassifier.train(train_set)

    def time_question_features(self, text):
        """
        Provide an analysis of significant features in the string.
        """
        features = {}

        # A list of all words from the known sentences
        all_words = " ".join(self.positive + self.negative).split()

        # A list of the first word in each of the known sentence
        all_first_words = []
        for sentence in self.positive + self.negative:
            all_first_words.append(
                sentence.split(' ', 1)[0]
            )

        for word in text.split():
            features['first_word({})'.format(word)] = (word in all_first_words)

        for word in text.split():
            features['contains({})'.format(word)] = (word in all_words)

        for letter in 'abcdefghijklmnopqrstuvwxyz':
            features['count({})'.format(letter)] = text.lower().count(letter)
            features['has({})'.format(letter)] = (letter in text.lower())

        return features

    def process(self, statement, additional_response_selection_parameters=None):
        now = datetime.now()

        time_features = self.time_question_features(statement.text.lower())
        confidence = self.classifier.classify(time_features)
        response = Statement(text='The current time is ' + now.strftime('%I:%M %p'))

        response.confidence = confidence
        return response



File: chatterbot/storage/sql_storage.py

from chatterbot.storage import StorageAdapter


class SQLStorageAdapter(StorageAdapter):
    """
    The SQLStorageAdapter allows ChatterBot to store conversation
    data in any database supported by the SQL Alchemy ORM.

    All parameters are optional, by default a sqlite database is used.

    It will check if tables are present, if they are not, it will attempt
    to create the required tables.

    :keyword database_uri: eg: sqlite:///database_test.sqlite3',
        The database_uri can be specified to choose database driver.
    :type database_uri: str
    """

    def __init__(self, **kwargs):
        super().__init__(**kwargs)

        from sqlalchemy import create_engine
        from sqlalchemy.orm import sessionmaker

        self.database_uri = kwargs.get('database_uri', False)

        # None results in a sqlite in-memory database as the default
        if self.database_uri is None:
            self.database_uri = 'sqlite://'

        # Create a file database if the database is not a connection string
        if not self.database_uri:
            self.database_uri = 'sqlite:///db.sqlite3'

        self.engine = create_engine(self.database_uri, convert_unicode=True)

        if self.database_uri.startswith('sqlite://'):
            from sqlalchemy.engine import Engine
            from sqlalchemy import event

            @event.listens_for(Engine, 'connect')
            def set_sqlite_pragma(dbapi_connection, connection_record):
                dbapi_connection.execute('PRAGMA journal_mode=WAL')
                dbapi_connection.execute('PRAGMA synchronous=NORMAL')

        if not self.engine.dialect.has_table(self.engine, 'Statement'):
            self.create_database()

        self.Session = sessionmaker(bind=self.engine, expire_on_commit=True)

    def get_statement_model(self):
        """
        Return the statement model.
        """
        from chatterbot.ext.sqlalchemy_app.models import Statement
        return Statement

    def get_tag_model(self):
        """
        Return the conversation model.
        """
        from chatterbot.ext.sqlalchemy_app.models import Tag
        return Tag

    def model_to_object(self, statement):
        from chatterbot.conversation import Statement as StatementObject

        return StatementObject(**statement.serialize())

    def count(self):
        """
        Return the number of entries in the database.
        """
        Statement = self.get_model('statement')

        session = self.Session()
        statement_count = session.query(Statement).count()
        session.close()
        return statement_count

    def remove(self, statement_text):
        """
        Removes the statement that matches the input text.
        Removes any responses from statements where the response text matches
        the input text.
        """
        Statement = self.get_model('statement')
        session = self.Session()

        query = session.query(Statement).filter_by(text=statement_text)
        record = query.first()

        session.delete(record)

        self._session_finish(session)

    def filter(self, **kwargs):
        """
        Returns a list of objects from the database.
        The kwargs parameter can contain any number
        of attributes. Only objects which contain all
        listed attributes and in which all values match
        for all listed attributes will be returned.
        """
        from sqlalchemy import or_

        Statement = self.get_model('statement')
        Tag = self.get_model('tag')

        session = self.Session()

        page_size = kwargs.pop('page_size', 1000)
        order_by = kwargs.pop('order_by', None)
        tags = kwargs.pop('tags', [])
        exclude_text = kwargs.pop('exclude_text', None)
        exclude_text_words = kwargs.pop('exclude_text_words', [])
        persona_not_startswith = kwargs.pop('persona_not_startswith', None)
        search_text_contains = kwargs.pop('search_text_contains', None)

        # Convert a single sting into a list if only one tag is provided
        if type(tags) == str:
            tags = [tags]

        if len(kwargs) == 0:
            statements = session.query(Statement).filter()
        else:
            statements = session.query(Statement).filter_by(**kwargs)

        if tags:
            statements = statements.join(Statement.tags).filter(
                Tag.name.in_(tags)
            )

        if exclude_text:
            statements = statements.filter(
                ~Statement.text.in_(exclude_text)
            )

        if exclude_text_words:
            or_word_query = [
                Statement.text.ilike('%' + word + '%') for word in exclude_text_words
            ]
            statements = statements.filter(
                ~or_(*or_word_query)
            )

        if persona_not_startswith:
            statements = statements.filter(
                ~Statement.persona.startswith('bot:')
            )

        if search_text_contains:
            or_query = [
                Statement.search_text.contains(word) for word in search_text_contains.split(' ')
            ]
            statements = statements.filter(
                or_(*or_query)
            )

        if order_by:

            if 'created_at' in order_by:
                index = order_by.index('created_at')
                order_by[index] = Statement.created_at.asc()

            statements = statements.order_by(*order_by)

        total_statements = statements.count()

        for start_index in range(0, total_statements, page_size):
            for statement in statements.slice(start_index, start_index + page_size):
                yield self.model_to_object(statement)

        session.close()

    def create(self, **kwargs):
        """
        Creates a new statement matching the keyword arguments specified.
        Returns the created statement.
        """
        Statement = self.get_model('statement')
        Tag = self.get_model('tag')

        session = self.Session()

        tags = set(kwargs.pop('tags', []))

        if 'search_text' not in kwargs:
            kwargs['search_text'] = self.tagger.get_text_index_string(kwargs['text'])

        if 'search_in_response_to' not in kwargs:
            in_response_to = kwargs.get('in_response_to')
            if in_response_to:
                kwargs['search_in_response_to'] = self.tagger.get_text_index_string(in_response_to)

        statement = Statement(**kwargs)

        for tag_name in tags:
            tag = session.query(Tag).filter_by(name=tag_name).first()

            if not tag:
                # Create the tag
                tag = Tag(name=tag_name)

            statement.tags.append(tag)

        session.add(statement)

        session.flush()

        session.refresh(statement)

        statement_object = self.model_to_object(statement)

        self._session_finish(session)

        return statement_object

    def create_many(self, statements):
        """
        Creates multiple statement entries.
        """
        Statement = self.get_model('statement')
        Tag = self.get_model('tag')

        session = self.Session()

        create_statements = []
        create_tags = {}

        for statement in statements:

            statement_data = statement.serialize()
            tag_data = statement_data.pop('tags', [])

            statement_model_object = Statement(**statement_data)

            if not statement.search_text:
                statement_model_object.search_text = self.tagger.get_text_index_string(statement.text)

            if not statement.search_in_response_to and statement.in_response_to:
                statement_model_object.search_in_response_to = self.tagger.get_text_index_string(statement.in_response_to)

            new_tags = set(tag_data) - set(create_tags.keys())

            if new_tags:
                existing_tags = session.query(Tag).filter(
                    Tag.name.in_(new_tags)
                )

                for existing_tag in existing_tags:
                    create_tags[existing_tag.name] = existing_tag

            for tag_name in tag_data:
                if tag_name in create_tags:
                    tag = create_tags[tag_name]
                else:
                    # Create the tag if it does not exist
                    tag = Tag(name=tag_name)

                    create_tags[tag_name] = tag

                statement_model_object.tags.append(tag)
            create_statements.append(statement_model_object)

        session.add_all(create_statements)
        session.commit()

    def update(self, statement):
        """
        Modifies an entry in the database.
        Creates an entry if one does not exist.
        """
        Statement = self.get_model('statement')
        Tag = self.get_model('tag')

        if statement is not None:
            session = self.Session()
            record = None

            if hasattr(statement, 'id') and statement.id is not None:
                record = session.query(Statement).get(statement.id)
            else:
                record = session.query(Statement).filter(
                    Statement.text == statement.text,
                    Statement.conversation == statement.conversation,
                ).first()

                # Create a new statement entry if one does not already exist
                if not record:
                    record = Statement(
                        text=statement.text,
                        conversation=statement.conversation,
                        persona=statement.persona
                    )

            # Update the response value
            record.in_response_to = statement.in_response_to

            record.created_at = statement.created_at

            record.search_text = self.tagger.get_text_index_string(statement.text)

            if statement.in_response_to:
                record.search_in_response_to = self.tagger.get_text_index_string(statement.in_response_to)

            for tag_name in statement.get_tags():
                tag = session.query(Tag).filter_by(name=tag_name).first()

                if not tag:
                    # Create the record
                    tag = Tag(name=tag_name)

                record.tags.append(tag)

            session.add(record)

            self._session_finish(session)

    def get_random(self):
        """
        Returns a random statement from the database.
        """
        import random

        Statement = self.get_model('statement')

        session = self.Session()
        count = self.count()
        if count < 1:
            raise self.EmptyDatabaseException()

        random_index = random.randrange(0, count)
        random_statement = session.query(Statement)[random_index]

        statement = self.model_to_object(random_statement)

        session.close()
        return statement

    def drop(self):
        """
        Drop the database.
        """
        Statement = self.get_model('statement')
        Tag = self.get_model('tag')

        session = self.Session()

        session.query(Statement).delete()
        session.query(Tag).delete()

        session.commit()
        session.close()

    def create_database(self):
        """
        Populate the database with the tables.
        """
        from chatterbot.ext.sqlalchemy_app.models import Base
        Base.metadata.create_all(self.engine)

    def _session_finish(self, session, statement_text=None):
        from sqlalchemy.exc import InvalidRequestError
        try:
            session.commit()
        except InvalidRequestError:
            # Log the statement text and the exception
            self.logger.exception(statement_text)
        finally:
            session.close()



File: chatterbot/storage/mongodb.py

import re
from chatterbot.storage import StorageAdapter


class MongoDatabaseAdapter(StorageAdapter):
    """
    The MongoDatabaseAdapter is an interface that allows
    ChatterBot to store statements in a MongoDB database.

    :keyword database_uri: The URI of a remote instance of MongoDB.
                           This can be any valid
                           `MongoDB connection string <https://docs.mongodb.com/manual/reference/connection-string/>`_
    :type database_uri: str

    .. code-block:: python

       database_uri='mongodb://example.com:8100/'
    """

    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        from pymongo import MongoClient
        from pymongo.errors import OperationFailure

        self.database_uri = kwargs.get(
            'database_uri', 'mongodb://localhost:27017/chatterbot-database'
        )

        # Use the default host and port
        self.client = MongoClient(self.database_uri)

        # Increase the sort buffer to 42M if possible
        try:
            self.client.admin.command({'setParameter': 1, 'internalQueryExecMaxBlockingSortBytes': 44040192})
        except OperationFailure:
            pass

        # Specify the name of the database
        self.database = self.client.get_database()

        # The mongo collection of statement documents
        self.statements = self.database['statements']

    def get_statement_model(self):
        """
        Return the class for the statement model.
        """
        from chatterbot.conversation import Statement

        # Create a storage-aware statement
        statement = Statement
        statement.storage = self

        return statement

    def count(self):
        return self.statements.count()

    def mongo_to_object(self, statement_data):
        """
        Return Statement object when given data
        returned from Mongo DB.
        """
        Statement = self.get_model('statement')

        statement_data['id'] = statement_data['_id']

        return Statement(**statement_data)

    def filter(self, **kwargs):
        """
        Returns a list of statements in the database
        that match the parameters specified.
        """
        import pymongo

        page_size = kwargs.pop('page_size', 1000)
        order_by = kwargs.pop('order_by', None)
        tags = kwargs.pop('tags', [])
        exclude_text = kwargs.pop('exclude_text', None)
        exclude_text_words = kwargs.pop('exclude_text_words', [])
        persona_not_startswith = kwargs.pop('persona_not_startswith', None)
        search_text_contains = kwargs.pop('search_text_contains', None)

        if tags:
            kwargs['tags'] = {
                '$in': tags
            }

        if exclude_text:
            if 'text' not in kwargs:
                kwargs['text'] = {}
            elif 'text' in kwargs and isinstance(kwargs['text'], str):
                text = kwargs.pop('text')
                kwargs['text'] = {
                    '$eq': text
                }
            kwargs['text']['$nin'] = exclude_text

        if exclude_text_words:
            if 'text' not in kwargs:
                kwargs['text'] = {}
            elif 'text' in kwargs and isinstance(kwargs['text'], str):
                text = kwargs.pop('text')
                kwargs['text'] = {
                    '$eq': text
                }
            exclude_word_regex = '|'.join([
                '.*{}.*'.format(word) for word in exclude_text_words
            ])
            kwargs['text']['$not'] = re.compile(exclude_word_regex)

        if persona_not_startswith:
            if 'persona' not in kwargs:
                kwargs['persona'] = {}
            elif 'persona' in kwargs and isinstance(kwargs['persona'], str):
                persona = kwargs.pop('persona')
                kwargs['persona'] = {
                    '$eq': persona
                }
            kwargs['persona']['$not'] = re.compile('^bot:*')

        if search_text_contains:
            or_regex = '|'.join([
                '{}'.format(re.escape(word)) for word in search_text_contains.split(' ')
            ])
            kwargs['search_text'] = re.compile(or_regex)

        mongo_ordering = []

        if order_by:

            # Sort so that newer datetimes appear first
            if 'created_at' in order_by:
                order_by.remove('created_at')
                mongo_ordering.append(('created_at', pymongo.DESCENDING, ))

            for order in order_by:
                mongo_ordering.append((order, pymongo.ASCENDING))

        total_statements = self.statements.find(kwargs).count()

        for start_index in range(0, total_statements, page_size):
            if mongo_ordering:
                for match in self.statements.find(kwargs).sort(mongo_ordering).skip(start_index).limit(page_size):
                    yield self.mongo_to_object(match)
            else:
                for match in self.statements.find(kwargs).skip(start_index).limit(page_size):
                    yield self.mongo_to_object(match)

    def create(self, **kwargs):
        """
        Creates a new statement matching the keyword arguments specified.
        Returns the created statement.
        """
        Statement = self.get_model('statement')

        if 'tags' in kwargs:
            kwargs['tags'] = list(set(kwargs['tags']))

        if 'search_text' not in kwargs:
            kwargs['search_text'] = self.tagger.get_text_index_string(kwargs['text'])

        if 'search_in_response_to' not in kwargs:
            if kwargs.get('in_response_to'):
                kwargs['search_in_response_to'] = self.tagger.get_text_index_string(kwargs['in_response_to'])

        inserted = self.statements.insert_one(kwargs)

        kwargs['id'] = inserted.inserted_id

        return Statement(**kwargs)

    def create_many(self, statements):
        """
        Creates multiple statement entries.
        """
        create_statements = []

        for statement in statements:
            statement_data = statement.serialize()
            tag_data = list(set(statement_data.pop('tags', [])))
            statement_data['tags'] = tag_data

            if not statement.search_text:
                statement_data['search_text'] = self.tagger.get_text_index_string(statement.text)

            if not statement.search_in_response_to and statement.in_response_to:
                statement_data['search_in_response_to'] = self.tagger.get_text_index_string(statement.in_response_to)

            create_statements.append(statement_data)

        self.statements.insert_many(create_statements)

    def update(self, statement):
        data = statement.serialize()
        data.pop('id', None)
        data.pop('tags', None)

        data['search_text'] = self.tagger.get_text_index_string(data['text'])

        if data.get('in_response_to'):
            data['search_in_response_to'] = self.tagger.get_text_index_string(data['in_response_to'])

        update_data = {
            '$set': data
        }

        if statement.tags:
            update_data['$addToSet'] = {
                'tags': {
                    '$each': statement.tags
                }
            }

        search_parameters = {}

        if statement.id is not None:
            search_parameters['_id'] = statement.id
        else:
            search_parameters['text'] = statement.text
            search_parameters['conversation'] = statement.conversation

        update_operation = self.statements.update_one(
            search_parameters,
            update_data,
            upsert=True
        )

        if update_operation.acknowledged:
            statement.id = update_operation.upserted_id

        return statement

    def get_random(self):
        """
        Returns a random statement from the database
        """
        from random import randint

        count = self.count()

        if count < 1:
            raise self.EmptyDatabaseException()

        random_integer = randint(0, count - 1)

        statements = self.statements.find().limit(1).skip(random_integer)

        return self.mongo_to_object(list(statements)[0])

    def remove(self, statement_text):
        """
        Removes the statement that matches the input text.
        """
        self.statements.delete_one({'text': statement_text})

    def drop(self):
        """
        Remove the database.
        """
        self.client.drop_database(self.database.name)



File: chatterbot/storage/__init__.py

from chatterbot.storage.storage_adapter import StorageAdapter
from chatterbot.storage.django_storage import DjangoStorageAdapter
from chatterbot.storage.mongodb import MongoDatabaseAdapter
from chatterbot.storage.sql_storage import SQLStorageAdapter


__all__ = (
    'StorageAdapter',
    'DjangoStorageAdapter',
    'MongoDatabaseAdapter',
    'SQLStorageAdapter',
)



File: chatterbot/storage/storage_adapter.py

import logging
from chatterbot import languages
from chatterbot.tagging import PosLemmaTagger


class StorageAdapter(object):
    """
    This is an abstract class that represents the interface
    that all storage adapters should implement.
    """

    def __init__(self, *args, **kwargs):
        """
        Initialize common attributes shared by all storage adapters.

        :param str tagger_language: The language that the tagger uses to remove stopwords.
        """
        self.logger = kwargs.get('logger', logging.getLogger(__name__))

        Tagger = kwargs.get('tagger', PosLemmaTagger)

        self.tagger = Tagger(language=kwargs.get(
            'tagger_language', languages.ENG
        ))

    def get_model(self, model_name):
        """
        Return the model class for a given model name.

        model_name is case insensitive.
        """
        get_model_method = getattr(self, 'get_%s_model' % (
            model_name.lower(),
        ))

        return get_model_method()

    def get_object(self, object_name):
        """
        Return the class for a given object name.

        object_name is case insensitive.
        """
        get_model_method = getattr(self, 'get_%s_object' % (
            object_name.lower(),
        ))

        return get_model_method()

    def get_statement_object(self):
        from chatterbot.conversation import Statement

        StatementModel = self.get_model('statement')

        Statement.statement_field_names.extend(
            StatementModel.extra_statement_field_names
        )

        return Statement

    def count(self):
        """
        Return the number of entries in the database.
        """
        raise self.AdapterMethodNotImplementedError(
            'The `count` method is not implemented by this adapter.'
        )

    def remove(self, statement_text):
        """
        Removes the statement that matches the input text.
        Removes any responses from statements where the response text matches
        the input text.
        """
        raise self.AdapterMethodNotImplementedError(
            'The `remove` method is not implemented by this adapter.'
        )

    def filter(self, **kwargs):
        """
        Returns a list of objects from the database.
        The kwargs parameter can contain any number
        of attributes. Only objects which contain
        all listed attributes and in which all values
        match for all listed attributes will be returned.

        :param page_size: The maximum number of records to load into
            memory at once when returning results.
            Defaults to 1000

        :param order_by: The field name that should be used to determine
            the order that results are returned in.
            Defaults to None

        :param tags: A list of tags. When specified, the results will only
            include statements that have a tag in the provided list.
            Defaults to [] (empty list)

        :param exclude_text: If the ``text`` of a statement is an exact match
            for the value of this parameter the statement will not be
            included in the result set.
            Defaults to None

        :param exclude_text_words: If the ``text`` of a statement contains a
            word from this list then the statement will not be included in
            the result set.
            Defaults to [] (empty list)

        :param persona_not_startswith: If the ``persona`` field of a
            statement starts with the value specified by this parameter,
            then the statement will not be returned in the result set.
            Defaults to None

        :param search_text_contains: If the ``search_text`` field of a
            statement contains a word that is in the string provided to
            this parameter, then the statement will be included in the
            result set.
            Defaults to None
        """
        raise self.AdapterMethodNotImplementedError(
            'The `filter` method is not implemented by this adapter.'
        )

    def create(self, **kwargs):
        """
        Creates a new statement matching the keyword arguments specified.
        Returns the created statement.
        """
        raise self.AdapterMethodNotImplementedError(
            'The `create` method is not implemented by this adapter.'
        )

    def create_many(self, statements):
        """
        Creates multiple statement entries.
        """
        raise self.AdapterMethodNotImplementedError(
            'The `create_many` method is not implemented by this adapter.'
        )

    def update(self, statement):
        """
        Modifies an entry in the database.
        Creates an entry if one does not exist.
        """
        raise self.AdapterMethodNotImplementedError(
            'The `update` method is not implemented by this adapter.'
        )

    def get_random(self):
        """
        Returns a random statement from the database.
        """
        raise self.AdapterMethodNotImplementedError(
            'The `get_random` method is not implemented by this adapter.'
        )

    def drop(self):
        """
        Drop the database attached to a given adapter.
        """
        raise self.AdapterMethodNotImplementedError(
            'The `drop` method is not implemented by this adapter.'
        )

    class EmptyDatabaseException(Exception):

        def __init__(self, message=None):
            default = 'The database currently contains no entries. At least one entry is expected. You may need to train your chat bot to populate your database.'
            super().__init__(message or default)

    class AdapterMethodNotImplementedError(NotImplementedError):
        """
        An exception to be raised when a storage adapter method has not been implemented.
        Typically this indicates that the method should be implement in a subclass.
        """
        pass



File: chatterbot/storage/django_storage.py

from chatterbot.storage import StorageAdapter
from chatterbot import constants


class DjangoStorageAdapter(StorageAdapter):
    """
    Storage adapter that allows ChatterBot to interact with
    Django storage backends.
    """

    def __init__(self, **kwargs):
        super().__init__(**kwargs)

        self.django_app_name = kwargs.get(
            'django_app_name',
            constants.DEFAULT_DJANGO_APP_NAME
        )

    def get_statement_model(self):
        from django.apps import apps
        return apps.get_model(self.django_app_name, 'Statement')

    def get_tag_model(self):
        from django.apps import apps
        return apps.get_model(self.django_app_name, 'Tag')

    def count(self):
        Statement = self.get_model('statement')
        return Statement.objects.count()

    def filter(self, **kwargs):
        """
        Returns a list of statements in the database
        that match the parameters specified.
        """
        from django.db.models import Q

        Statement = self.get_model('statement')

        kwargs.pop('page_size', 1000)
        order_by = kwargs.pop('order_by', None)
        tags = kwargs.pop('tags', [])
        exclude_text = kwargs.pop('exclude_text', None)
        exclude_text_words = kwargs.pop('exclude_text_words', [])
        persona_not_startswith = kwargs.pop('persona_not_startswith', None)
        search_text_contains = kwargs.pop('search_text_contains', None)

        # Convert a single sting into a list if only one tag is provided
        if type(tags) == str:
            tags = [tags]

        if tags:
            kwargs['tags__name__in'] = tags

        statements = Statement.objects.filter(**kwargs)

        if exclude_text:
            statements = statements.exclude(
                text__in=exclude_text
            )

        if exclude_text_words:
            or_query = [
                ~Q(text__icontains=word) for word in exclude_text_words
            ]

            statements = statements.filter(
                *or_query
            )

        if persona_not_startswith:
            statements = statements.exclude(
                persona__startswith='bot:'
            )

        if search_text_contains:
            or_query = Q()

            for word in search_text_contains.split(' '):
                or_query |= Q(search_text__contains=word)

            statements = statements.filter(
                or_query
            )

        if order_by:
            statements = statements.order_by(*order_by)

        for statement in statements.iterator():
            yield statement

    def create(self, **kwargs):
        """
        Creates a new statement matching the keyword arguments specified.
        Returns the created statement.
        """
        Statement = self.get_model('statement')
        Tag = self.get_model('tag')

        tags = kwargs.pop('tags', [])

        if 'search_text' not in kwargs:
            kwargs['search_text'] = self.tagger.get_text_index_string(kwargs['text'])

        if 'search_in_response_to' not in kwargs:
            if kwargs.get('in_response_to'):
                kwargs['search_in_response_to'] = self.tagger.get_text_index_string(kwargs['in_response_to'])

        statement = Statement(**kwargs)

        statement.save()

        tags_to_add = []

        for _tag in tags:
            tag, _ = Tag.objects.get_or_create(name=_tag)
            tags_to_add.append(tag)

        statement.tags.add(*tags_to_add)

        return statement

    def create_many(self, statements):
        """
        Creates multiple statement entries.
        """
        Statement = self.get_model('statement')
        Tag = self.get_model('tag')

        tag_cache = {}

        for statement in statements:

            statement_data = statement.serialize()
            tag_data = statement_data.pop('tags', [])

            statement_model_object = Statement(**statement_data)

            if not statement.search_text:
                statement_model_object.search_text = self.tagger.get_text_index_string(statement.text)

            if not statement.search_in_response_to and statement.in_response_to:
                statement_model_object.search_in_response_to = self.tagger.get_text_index_string(statement.in_response_to)

            statement_model_object.save()

            tags_to_add = []

            for tag_name in tag_data:
                if tag_name in tag_cache:
                    tag = tag_cache[tag_name]
                else:
                    tag, _ = Tag.objects.get_or_create(name=tag_name)
                    tag_cache[tag_name] = tag
                tags_to_add.append(tag)

            statement_model_object.tags.add(*tags_to_add)

    def update(self, statement):
        """
        Update the provided statement.
        """
        Statement = self.get_model('statement')
        Tag = self.get_model('tag')

        if hasattr(statement, 'id'):
            statement.save()
        else:
            statement = Statement.objects.create(
                text=statement.text,
                search_text=self.tagger.get_text_index_string(statement.text),
                conversation=statement.conversation,
                in_response_to=statement.in_response_to,
                search_in_response_to=self.tagger.get_text_index_string(statement.in_response_to),
                created_at=statement.created_at
            )

        for _tag in statement.tags.all():
            tag, _ = Tag.objects.get_or_create(name=_tag)

            statement.tags.add(tag)

        return statement

    def get_random(self):
        """
        Returns a random statement from the database
        """
        Statement = self.get_model('statement')

        statement = Statement.objects.order_by('?').first()

        if statement is None:
            raise self.EmptyDatabaseException()

        return statement

    def remove(self, statement_text):
        """
        Removes the statement that matches the input text.
        Removes any responses from statements if the response text matches the
        input text.
        """
        Statement = self.get_model('statement')

        statements = Statement.objects.filter(text=statement_text)

        statements.delete()

    def drop(self):
        """
        Remove all data from the database.
        """
        Statement = self.get_model('statement')
        Tag = self.get_model('tag')

        Statement.objects.all().delete()
        Tag.objects.all().delete()



![ChatterBot: Machine learning in Python](https://i.imgur.com/b3SCmGT.png)

# ChatterBot

ChatterBot is a machine-learning based conversational dialog engine build in
Python which makes it possible to generate responses based on collections of
known conversations. The language independent design of ChatterBot allows it
to be trained to speak any language.

[![Package Version](https://img.shields.io/pypi/v/chatterbot.svg)](https://pypi.python.org/pypi/chatterbot/)
[![Python 3.6](https://img.shields.io/badge/python-3.6-blue.svg)](https://www.python.org/downloads/release/python-360/)
[![Django 2.0](https://img.shields.io/badge/Django-2.0-blue.svg)](https://docs.djangoproject.com/en/2.1/releases/2.0/)
[![Requirements Status](https://requires.io/github/gunthercox/ChatterBot/requirements.svg?branch=master)](https://requires.io/github/gunthercox/ChatterBot/requirements/?branch=master)
[![Build Status](https://travis-ci.org/gunthercox/ChatterBot.svg?branch=master)](https://travis-ci.org/gunthercox/ChatterBot)
[![Documentation Status](https://readthedocs.org/projects/chatterbot/badge/?version=stable)](http://chatterbot.readthedocs.io/en/stable/?badge=stable)
[![Coverage Status](https://img.shields.io/coveralls/gunthercox/ChatterBot.svg)](https://coveralls.io/r/gunthercox/ChatterBot)
[![Code Climate](https://codeclimate.com/github/gunthercox/ChatterBot/badges/gpa.svg)](https://codeclimate.com/github/gunthercox/ChatterBot)
[![Join the chat at https://gitter.im/chatterbot/Lobby](https://badges.gitter.im/chatterbot/Lobby.svg)](https://gitter.im/chatterbot/Lobby?utm_source=badge&utm_medium=badge&utm_content=badge)

An example of typical input would be something like this:

> **user:** Good morning! How are you doing?  
> **bot:**  I am doing very well, thank you for asking.  
> **user:** You're welcome.  
> **bot:** Do you like hats?  

## How it works

An untrained instance of ChatterBot starts off with no knowledge of how to communicate. Each time a user enters a statement, the library saves the text that they entered and the text that the statement was in response to. As ChatterBot receives more input the number of responses that it can reply and the accuracy of each response in relation to the input statement increase. The program selects the closest matching response by searching for the closest matching known statement that matches the input, it then returns the most likely response to that statement based on how frequently each response is issued by the people the bot communicates with.

## Installation

This package can be installed from [PyPi](https://pypi.python.org/pypi/ChatterBot) by running:

```
pip install chatterbot
```

## Basic Usage

```
from chatterbot import ChatBot
from chatterbot.trainers import ChatterBotCorpusTrainer

chatbot = ChatBot('Ron Obvious')

# Create a new trainer for the chatbot
trainer = ChatterBotCorpusTrainer(chatbot)

# Train the chatbot based on the english corpus
trainer.train("chatterbot.corpus.english")

# Get a response to an input statement
chatbot.get_response("Hello, how are you today?")
```

# Training data

ChatterBot comes with a data utility module that can be used to train chat bots.
At the moment there is training data for over a dozen languages in this module.
Contributions of additional training data or training data
in other languages would be greatly appreciated. Take a look at the data files
in the [chatterbot-corpus](https://github.com/gunthercox/chatterbot-corpus)
package if you are interested in contributing.

```
from chatterbot.trainers import ChatterBotCorpusTrainer

# Create a new trainer for the chatbot
trainer = ChatterBotCorpusTrainer(chatbot)

# Train based on the english corpus
trainer.train("chatterbot.corpus.english")

# Train based on english greetings corpus
trainer.train("chatterbot.corpus.english.greetings")

# Train based on the english conversations corpus
trainer.train("chatterbot.corpus.english.conversations")
```

**Corpus contributions are welcome! Please make a pull request.**

# [Documentation](https://chatterbot.readthedocs.io/)

View the [documentation](https://chatterbot.readthedocs.io/)
for ChatterBot on Read the Docs.

To build the documentation yourself using [Sphinx](http://www.sphinx-doc.org/), run:

```
sphinx-build -b html docs/ build/
```

# Examples

For examples, see the [examples](https://github.com/gunthercox/ChatterBot/tree/master/examples)
directory in this project's git repository.

There is also an example [Django project using ChatterBot](https://github.com/gunthercox/ChatterBot/tree/master/examples), as well as an example [Flask project using ChatterBot](https://github.com/chamkank/flask-chatterbot).

# History

See release notes for changes https://github.com/gunthercox/ChatterBot/releases

# Development pattern for contributors

1. [Create a fork](https://help.github.com/articles/fork-a-repo/) of
   the [main ChatterBot repository](https://github.com/gunthercox/ChatterBot) on GitHub.
2. Make your changes in a branch named something different from `master`, e.g. create
   a new branch `my-pull-request`.
3. [Create a pull request](https://help.github.com/articles/creating-a-pull-request/).
4. Please follow the [Python style guide for PEP-8](https://www.python.org/dev/peps/pep-0008/).
5. Use the projects [built-in automated testing](https://chatterbot.readthedocs.io/en/latest/testing.html).
   to help make sure that your contribution is free from errors.

# License

ChatterBot is licensed under the [BSD 3-clause license](https://opensource.org/licenses/BSD-3-Clause).
