[
  {
    "path": ".codeclimate.yml",
    "content": "engines:\n  pep8:\n    enabled: true\nratings:\n  paths:\n  - \"**.py\"\nexclude_paths:\n- tests/*\n- examples/*\n- chatterbot/ext/django_chatterbot/migrations/*\n"
  },
  {
    "path": ".github/FUNDING.yml",
    "content": "github: gunthercox\n"
  },
  {
    "path": ".github/workflows/publish-documentation.yml",
    "content": "name: Deploy Sphinx documentation to Pages\n\non:\n  push:\n    branches: [master] # branch to trigger deployment\n\njobs:\n  pages:\n    runs-on: ubuntu-latest\n    environment:\n      name: github-pages\n      url: ${{ steps.deployment.outputs.page_url }}\n    permissions:\n      pages: write\n      id-token: write\n    steps:\n    - id: deployment\n      uses: sphinx-notes/pages@v3\n      with:\n        pyproject_extras: 'test'\n        sphinx_build_options: '-b dirhtml'\n"
  },
  {
    "path": ".github/workflows/python-package.yml",
    "content": "# This workflow will install Python dependencies, run tests and lint with a variety of Python versions\n# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python\n\nname: Python package\n\npermissions:\n  contents: read\n  pull-requests: write\n  checks: write\n\non:\n  push:\n    branches: [ \"master\" ]\n  pull_request:\n    branches: [ \"*\" ]\nenv:\n  CHATTERBOT_SHOW_TRAINING_PROGRESS: '0'\n\njobs:\n\n  build:\n    runs-on: ubuntu-latest\n    strategy:\n      fail-fast: false\n      matrix:\n        python-version: [\"3.9\", \"3.10\", \"3.11\", \"3.12\", \"3.13\"]\n    services:\n      redis:\n        image: redis/redis-stack-server:latest\n        ports:\n          - 6379:6379\n    steps:\n    - uses: actions/checkout@v4\n    - name: Set up Python ${{ matrix.python-version }}\n      uses: actions/setup-python@v5\n      with:\n        python-version: ${{ matrix.python-version }}\n        cache: 'pip'\n    - name: Install dependencies\n      run: |\n        python -m pip install --upgrade pip\n        pip install .[test,dev,redis,mongodb]\n        python -m spacy download en_core_web_sm\n        python -m spacy download de_core_news_sm\n    - name: Lint with flake8\n      run: |\n        # stop the build if there are Python syntax errors or undefined names\n        flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics\n        # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide\n        flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics\n    - name: Start MongoDB\n      uses: supercharge/mongodb-github-action@1.11.0\n      with:\n        mongodb-version: '8.0'\n    - name: Run tests\n      run: |\n        python -Wonce -m unittest discover -s tests -v\n    - name: Run tests for Django example app\n      run: |\n        python -Wonce examples/django_example/manage.py test examples/django_example/\n    # --------------------------------------------------------------\n    # TODO: Fix & re-enable later\n    # https://github.com/marketplace/actions/coveralls-github-action\n    # - name: Coveralls GitHub Action\n    #   uses: coverallsapp/github-action@v2.3.4\n    # - name: Generate code coverage\n    #   uses: paambaati/codeclimate-action@v9.0.0\n    #   env:\n    #     CC_TEST_REPORTER_ID: 3ec30a156224df0f59620967241d9659086e918fd824f4f69b8ce7b55b5a590f\n    #   with:\n    #     coverageCommand: coverage\n    #     debug: true\n"
  },
  {
    "path": ".gitignore",
    "content": "bin\nbuild\nhtml\ndist\nvenv\n.env\n.out\n.coverage\n.python-version\n*.pyc\n*.swp\n*.egg-info\n*.egg/*\n*.eggs/*\n*.doctrees\n\n# Database files\n.database\n*.sqlite3\n*.sqlite3-*\n\n# IDE files\n.vscode\n"
  },
  {
    "path": "CODE_OF_CONDUCT.md",
    "content": "# Contributor Covenant Code of Conduct\n\n## Our Pledge\n\nWe as members, contributors, and leaders pledge to make participation in our\ncommunity a harassment-free experience for everyone, regardless of age, body\nsize, visible or invisible disability, ethnicity, sex characteristics, gender\nidentity and expression, level of experience, education, socio-economic status,\nnationality, personal appearance, race, religion, or sexual identity\nand orientation.\n\nWe pledge to act and interact in ways that contribute to an open, welcoming,\ndiverse, inclusive, and healthy community.\n\n## Our Standards\n\nExamples of behavior that contributes to a positive environment for our\ncommunity include:\n\n* Demonstrating empathy and kindness toward other people\n* Being respectful of differing opinions, viewpoints, and experiences\n* Giving and gracefully accepting constructive feedback\n* Accepting responsibility and apologizing to those affected by our mistakes,\n  and learning from the experience\n* Focusing on what is best not just for us as individuals, but for the\n  overall community\n\nExamples of unacceptable behavior include:\n\n* The use of sexualized language or imagery, and sexual attention or\n  advances of any kind\n* Trolling, insulting or derogatory comments, and personal or political attacks\n* Public or private harassment\n* Publishing others' private information, such as a physical or email\n  address, without their explicit permission\n* Other conduct which could reasonably be considered inappropriate in a\n  professional setting\n\n## Enforcement Responsibilities\n\nCommunity leaders are responsible for clarifying and enforcing our standards of\nacceptable behavior and will take appropriate and fair corrective action in\nresponse to any behavior that they deem inappropriate, threatening, offensive,\nor harmful.\n\nCommunity leaders have the right and responsibility to remove, edit, or reject\ncomments, commits, code, wiki edits, issues, and other contributions that are\nnot aligned to this Code of Conduct, and will communicate reasons for moderation\ndecisions when appropriate.\n\n## Scope\n\nThis Code of Conduct applies within all community spaces, and also applies when\nan individual is officially representing the community in public spaces.\nExamples of representing our community include using an official e-mail address,\nposting via an official social media account, or acting as an appointed\nrepresentative at an online or offline event.\n\n## Enforcement\n\nInstances of abusive, harassing, or otherwise unacceptable behavior may be\nreported to the community leaders responsible for enforcement at\ncommunity@chatterbot.us.\nAll complaints will be reviewed and investigated promptly and fairly.\n\nAll community leaders are obligated to respect the privacy and security of the\nreporter of any incident.\n\n## Enforcement Guidelines\n\nCommunity leaders will follow these Community Impact Guidelines in determining\nthe consequences for any action they deem in violation of this Code of Conduct:\n\n### 1. Correction\n\n**Community Impact**: Use of inappropriate language or other behavior deemed\nunprofessional or unwelcome in the community.\n\n**Consequence**: A private, written warning from community leaders, providing\nclarity around the nature of the violation and an explanation of why the\nbehavior was inappropriate. A public apology may be requested.\n\n### 2. Warning\n\n**Community Impact**: A violation through a single incident or series\nof actions.\n\n**Consequence**: A warning with consequences for continued behavior. No\ninteraction with the people involved, including unsolicited interaction with\nthose enforcing the Code of Conduct, for a specified period of time. This\nincludes avoiding interactions in community spaces as well as external channels\nlike social media. Violating these terms may lead to a temporary or\npermanent ban.\n\n### 3. Temporary Ban\n\n**Community Impact**: A serious violation of community standards, including\nsustained inappropriate behavior.\n\n**Consequence**: A temporary ban from any sort of interaction or public\ncommunication with the community for a specified period of time. No public or\nprivate interaction with the people involved, including unsolicited interaction\nwith those enforcing the Code of Conduct, is allowed during this period.\nViolating these terms may lead to a permanent ban.\n\n### 4. Permanent Ban\n\n**Community Impact**: Demonstrating a pattern of violation of community\nstandards, including sustained inappropriate behavior,  harassment of an\nindividual, or aggression toward or disparagement of classes of individuals.\n\n**Consequence**: A permanent ban from any sort of public interaction within\nthe community.\n\n## Attribution\n\nThis Code of Conduct is adapted from the [Contributor Covenant][homepage],\nversion 2.0, available at\nhttps://www.contributor-covenant.org/version/2/0/code_of_conduct.html.\n\nCommunity Impact Guidelines were inspired by [Mozilla's code of conduct\nenforcement ladder](https://github.com/mozilla/diversity).\n\n[homepage]: https://www.contributor-covenant.org\n\nFor answers to common questions about this code of conduct, see the FAQ at\nhttps://www.contributor-covenant.org/faq. Translations are available at\nhttps://www.contributor-covenant.org/translations.\n"
  },
  {
    "path": "LICENSE",
    "content": "Copyright (c) 2016 - 2025, Gunther Cox\nAll rights reserved.\n\nRedistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:\n\n* Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.\n\n* Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.\n\n* Neither the name of ChatterBot nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n"
  },
  {
    "path": "README.md",
    "content": "![ChatterBot: Machine learning in Python](https://i.imgur.com/b3SCmGT.png)\n\n# ChatterBot\n\nChatterBot is a machine-learning based conversational dialog engine built in\nPython which makes it possible to generate responses based on collections of\nknown conversations. The language independent design of ChatterBot allows it\nto be trained to speak any language.\n\n[![Package Version](https://img.shields.io/pypi/v/chatterbot.svg)](https://pypi.python.org/pypi/chatterbot/)\n[![Python 3.12](https://img.shields.io/badge/python-3.12-blue.svg)](https://www.python.org/downloads/release/python-360/)\n[![Coverage Status](https://img.shields.io/coveralls/gunthercox/ChatterBot.svg)](https://coveralls.io/r/gunthercox/ChatterBot)\n[![Follow on Bluesky](https://img.shields.io/badge/🦋%20Bluesky-1185fe)](https://bsky.app/profile/chatterbot.us)\n[![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)\n<!-- [![Code Climate](https://codeclimate.com/github/gunthercox/ChatterBot/badges/gpa.svg)](https://codeclimate.com/github/gunthercox/ChatterBot) -->\n\nAn example of typical input would be something like this:\n\n> **user:** Good morning! How are you doing?  \n> **bot:**  I am doing very well, thank you for asking.  \n> **user:** You're welcome.  \n> **bot:** Do you like hats?  \n\n## How it works\n\nAn 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 to, and the accuracy of each response in relation to the input statement increases. 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.\n\n# [Documentation](https://docs.chatterbot.us)\n\nView the [documentation](https://docs.chatterbot.us)\nfor ChatterBot.\n\n## Installation\n\nThis package can be installed from [PyPi](https://pypi.python.org/pypi/ChatterBot) by running:\n\n```bash\npip install chatterbot\n```\n\n## Basic Usage\n\n```python\nfrom chatterbot import ChatBot\nfrom chatterbot.trainers import ChatterBotCorpusTrainer\n\nchatbot = ChatBot('Ron Obvious')\n\n# Create a new trainer for the chatbot\ntrainer = ChatterBotCorpusTrainer(chatbot)\n\n# Train the chatbot based on the english corpus\ntrainer.train(\"chatterbot.corpus.english\")\n\n# Get a response to an input statement\nchatbot.get_response(\"Hello, how are you today?\")\n```\n\n# Training data\n\nChatterBot comes with a data utility module that can be used to train chat bots.\nAt the moment there is training data for over a dozen languages in this module.\nContributions of additional training data or training data\nin other languages would be greatly appreciated. Take a look at the data files\nin the [chatterbot-corpus](https://github.com/gunthercox/chatterbot-corpus)\npackage if you are interested in contributing.\n\n```python\nfrom chatterbot.trainers import ChatterBotCorpusTrainer\n\n# Create a new trainer for the chatbot\ntrainer = ChatterBotCorpusTrainer(chatbot)\n\n# Train based on the english corpus\ntrainer.train(\"chatterbot.corpus.english\")\n\n# Train based on english greetings corpus\ntrainer.train(\"chatterbot.corpus.english.greetings\")\n\n# Train based on the english conversations corpus\ntrainer.train(\"chatterbot.corpus.english.conversations\")\n```\n\n**Corpus contributions are welcome! Please make a pull request.**\n\n# Examples\n\nFor examples, see the [examples](https://docs.chatterbot.us/examples/)\nsection of the documentation.\n\n# History\n\nSee release notes for changes https://github.com/gunthercox/ChatterBot/releases\n\n# Contributing\n\nContributions are welcomed, to help ensure a smooth process please start with the contributing guidelines in our documentation:\nhttps://docs.chatterbot.us/contributing/\n\n# Sponsors\n\nChatterBot is sponsored by:\n\n<p>\n   <a href=\"https://www.testmuai.com/?utm_source=chatterbot&utm_medium=sponsor\" target=\"_blank\">\n      <img src=\"docs/_static/testmu-ai-white-logo.png\" style=\"vertical-align: middle;\" width=\"250\" height=\"80\" />\n   </a>\n</p>\n\n# License\n\nChatterBot is licensed under the [BSD 3-clause license](https://opensource.org/licenses/BSD-3-Clause).\n"
  },
  {
    "path": "SECURITY.md",
    "content": "# Security Policy\n\n## Supported Versions\n\nActively supported versions of ChatterBot can be determined using the following table.\n\n| Version | Supported          |\n| ------- | ------------------ |\n| 1.2.x   | :white_check_mark: |\n| < 1.2   | :x:                |\n\n## Reporting a Vulnerability\n\nChatterBot uses GitHub's private security vulnerability reporting to accept reports about potential security vulnerabilities.\n\nhttps://github.com/gunthercox/ChatterBot/security/advisories\n\nTo begin the process select the \"Report a vulnerability\" button on the security advisories page.\nOnce the report has been investigated an response plan will be issued based on the level of severity.\nA response can generally be expected within 24 hours of report submission.\n"
  },
  {
    "path": "chatterbot/__init__.py",
    "content": "\"\"\"\nChatterBot is a machine learning, conversational dialog engine.\n\"\"\"\nfrom .chatterbot import ChatBot\n\n\n__version__ = '1.2.12'\n\n__all__ = (\n    'ChatBot',\n)\n"
  },
  {
    "path": "chatterbot/__main__.py",
    "content": "\"\"\"\nExample usage for ChatterBot command line arguments:\n\npython -m chatterbot --help\n\"\"\"\n\nimport sys\n\n\ndef get_chatterbot_version():\n    \"\"\"\n    Return the version of the current package.\n    \"\"\"\n    from chatterbot import __version__\n\n    return __version__\n\n\nif __name__ == '__main__':\n    if '--version' in sys.argv:\n        print(get_chatterbot_version())\n    elif '--help' in sys.argv:\n        print('usage: chatterbot [--version, --help]')\n        print('  --version: Print the version of ChatterBot')\n        print('  --help: Print this help message')\n        print()\n        print('Documentation at https://docs.chatterbot.us')\n"
  },
  {
    "path": "chatterbot/adapters.py",
    "content": "class Adapter(object):\n    \"\"\"\n    A superclass for all adapter classes.\n\n    :param chatbot: A ChatBot instance.\n    \"\"\"\n\n    def __init__(self, chatbot, **kwargs):\n        self.chatbot = chatbot\n\n    class AdapterMethodNotImplementedError(NotImplementedError):\n        \"\"\"\n        An exception to be raised when an adapter method has not been implemented.\n        Typically this indicates that the developer is expected to implement the\n        method in a subclass.\n        \"\"\"\n\n        def __init__(self, message='This method must be overridden in a subclass method.'):\n            \"\"\"\n            Set the message for the exception.\n            \"\"\"\n            super().__init__(message)\n\n    class InvalidAdapterTypeException(Exception):\n        \"\"\"\n        An exception to be raised when an adapter\n        of an unexpected class type is received.\n        \"\"\"\n        pass\n"
  },
  {
    "path": "chatterbot/chatterbot.py",
    "content": "import logging\nimport uuid\nfrom typing import Union\nfrom chatterbot.storage import StorageAdapter\nfrom chatterbot.logic import LogicAdapter\nfrom chatterbot.search import TextSearch, IndexedTextSearch, SemanticVectorSearch\nfrom chatterbot.tagging import PosLemmaTagger\nfrom chatterbot.conversation import Statement\nfrom chatterbot import languages\nfrom chatterbot import utils\nimport spacy\n\n\nclass ChatBot(object):\n    \"\"\"\n    A conversational dialog chat bot.\n\n    :param name: A name is the only required parameter for the ChatBot class.\n    :type name: str\n\n    :keyword storage_adapter: The dot-notated import path to a storage adapter class.\n                              Defaults to ``\"chatterbot.storage.SQLStorageAdapter\"``.\n    :type storage_adapter: str\n\n    :param logic_adapters: A list of dot-notated import paths to each logic adapter the bot uses.\n                           Defaults to ``[\"chatterbot.logic.BestMatch\"]``.\n    :type logic_adapters: list\n\n    :param tagger: The tagger to use for the chat bot.\n                   Defaults to :class:`~chatterbot.tagging.PosLemmaTagger`\n    :type tagger: object\n\n    :param tagger_language: The language to use for the tagger.\n                            Defaults to :class:`~chatterbot.languages.ENG`.\n    :type tagger_language: object\n\n    :param preprocessors: A list of preprocessor functions to use for the chat bot.\n    :type preprocessors: list\n\n    :param read_only: If True, the chat bot will not save any input it receives, defaults to False.\n    :type read_only: bool\n\n    :param logger: A ``Logger`` object.\n    :type logger: logging.Logger\n\n    :param model: A definition used to load a large language model.\n                  Defaults to ``None``.\n                  (Added in version 1.2.7)\n    :type model: dict\n\n    :param stream: Return output as a streaming responses when a ``model`` is defined.\n                   (Added in version 1.2.7)\n    \"\"\"\n\n    def __init__(self, name, stream=False, **kwargs):\n        self.name = name\n\n        self.stream = stream\n\n        # Generate a default conversation ID for this ChatBot instance.\n        # This is used as a fallback when callers don't provide an explicit\n        # conversation ID, ensuring that conversation history is tracked\n        # within a session. Conversation IDs are necessary for cases such as\n        # the LLM-based logic adapters which require it to retrieve previous\n        # messages.\n        self.default_conversation = uuid.uuid4().hex\n\n        self.logger = kwargs.get('logger', logging.getLogger(__name__))\n\n        storage_adapter = kwargs.get('storage_adapter', 'chatterbot.storage.SQLStorageAdapter')\n\n        logic_adapters = kwargs.get('logic_adapters', [\n            'chatterbot.logic.BestMatch'\n        ])\n\n        # Check that each adapter is a valid subclass of it's respective parent\n        utils.validate_adapter_class(storage_adapter, StorageAdapter)\n\n        # Logic adapters used by the chat bot\n        self.logic_adapters = []\n\n        self.storage = utils.initialize_class(storage_adapter, **kwargs)\n\n        tagger_language = kwargs.get('tagger_language', languages.ENG)\n\n        # Check if storage adapter has a preferred tagger\n        PreferredTagger = self.storage.get_preferred_tagger()\n\n        if PreferredTagger is not None:\n            # Storage adapter specifies its own tagger\n            self.tagger = PreferredTagger(language=tagger_language)\n        else:\n            # Use default or user-specified tagger\n            try:\n                Tagger = kwargs.get('tagger', PosLemmaTagger)\n\n                # Allow instances to be provided for performance optimization\n                # (Example: a pre-loaded model in a tagger when unit testing)\n                if not isinstance(Tagger, type):\n                    self.tagger = Tagger\n                else:\n                    self.tagger = Tagger(language=tagger_language)\n            except IOError as io_error:\n                # Return a more helpful error message if possible\n                if \"Can't find model\" in str(io_error):\n                    model_name = utils.get_model_for_language(tagger_language)\n                    if hasattr(tagger_language, 'ENGLISH_NAME'):\n                        language_name = tagger_language.ENGLISH_NAME\n                    else:\n                        language_name = tagger_language\n                    raise self.ChatBotException(\n                        'Setup error:\\n'\n                        f'The Spacy model for \"{language_name}\" language is missing.\\n'\n                        'Please install the model using the command:\\n\\n'\n                        f'python -m spacy download {model_name}\\n\\n'\n                        'See https://spacy.io/usage/models for more information about available models.'\n                    ) from io_error\n                else:\n                    raise io_error\n\n        # Initialize search algorithms\n        primary_search_algorithm = IndexedTextSearch(self, **kwargs)\n        text_search_algorithm = TextSearch(self, **kwargs)\n        semantic_vector_search_algorithm = SemanticVectorSearch(self, **kwargs)\n\n        self.search_algorithms = {\n            primary_search_algorithm.name: primary_search_algorithm,\n            text_search_algorithm.name: text_search_algorithm,\n            semantic_vector_search_algorithm.name: semantic_vector_search_algorithm\n        }\n\n        # Check if storage adapter has a preferred search algorithm\n        preferred_search_algorithm = self.storage.get_preferred_search_algorithm()\n        if preferred_search_algorithm and preferred_search_algorithm in self.search_algorithms:\n            # Set as default for logic adapters that don't specify their own search algorithm\n            # This ensures BestMatch and other adapters use the optimal search method\n            self.logger.info(f'Storage adapter prefers search algorithm: {preferred_search_algorithm}')\n            kwargs.setdefault('search_algorithm_name', preferred_search_algorithm)\n\n        for adapter in logic_adapters:\n            utils.validate_adapter_class(adapter, LogicAdapter)\n            logic_adapter = utils.initialize_class(adapter, self, **kwargs)\n            self.logic_adapters.append(logic_adapter)\n\n        preprocessors = kwargs.get(\n            'preprocessors', [\n                'chatterbot.preprocessors.clean_whitespace'\n            ]\n        )\n\n        self.preprocessors = []\n\n        for preprocessor in preprocessors:\n            self.preprocessors.append(utils.import_module(preprocessor))\n\n        # NOTE: 'xx' is the language code for a multi-language model\n        self.nlp = spacy.blank(self.tagger.language.ISO_639_1)\n\n        # Allow the bot to save input it receives so that it can learn\n        self.read_only = kwargs.get('read_only', False)\n\n    def get_response(self, statement: Union[Statement, str, dict] = None, **kwargs) -> Statement:\n        \"\"\"\n        Return the bot's response based on the input.\n\n        :param statement: An statement object or string.\n        :returns: A response to the input.\n\n        :param additional_response_selection_parameters: Parameters to pass to the\n            chat bot's logic adapters to control response selection.\n        :type additional_response_selection_parameters: dict\n\n        :param persist_values_to_response: Values that should be saved to the response\n            that the chat bot generates.\n        :type persist_values_to_response: dict\n        \"\"\"\n        Statement = self.storage.get_object('statement')\n\n        additional_response_selection_parameters = kwargs.pop('additional_response_selection_parameters', {})\n\n        persist_values_to_response = kwargs.pop('persist_values_to_response', {})\n\n        if isinstance(statement, str):\n            kwargs['text'] = statement\n\n        if isinstance(statement, dict):\n            kwargs.update(statement)\n\n        if statement is None and 'text' not in kwargs:\n            raise self.ChatBotException(\n                'Either a statement object or a \"text\" keyword '\n                'argument is required. Neither was provided.'\n            )\n\n        if hasattr(statement, 'serialize'):\n            kwargs.update(**statement.serialize())\n\n        tags = kwargs.pop('tags', [])\n\n        text = kwargs.pop('text')\n\n        input_statement = Statement(text=text, **kwargs)\n\n        input_statement.add_tags(*tags)\n\n        # If no conversation ID was provided, use the default session ID\n        # so that conversation history is tracked across calls. Callers\n        # can override this by passing an explicit conversation kwarg or\n        # setting it on the Statement object.\n        if not input_statement.conversation:\n            input_statement.conversation = self.default_conversation\n\n        # Preprocess the input statement\n        for preprocessor in self.preprocessors:\n            input_statement = preprocessor(input_statement)\n\n        # Mark the statement as being a response to the previous\n        if input_statement.in_response_to is None:\n            previous_statement = self.get_latest_response(input_statement.conversation)\n            if previous_statement:\n                input_statement.in_response_to = previous_statement.text\n\n        # Make sure the input statement has its search text saved\n        if not self.tagger.needs_text_indexing():\n            # Tagger doesn't transform text, use it directly\n            if not input_statement.search_text:\n                input_statement.search_text = input_statement.text\n            if not input_statement.search_in_response_to and input_statement.in_response_to:\n                input_statement.search_in_response_to = input_statement.in_response_to\n        else:\n            # Use tagger for text indexing or transformations\n            if not input_statement.search_text:\n                _search_text = self.tagger.get_text_index_string(input_statement.text)\n                input_statement.search_text = _search_text\n\n            if not input_statement.search_in_response_to and input_statement.in_response_to:\n                input_statement.search_in_response_to = self.tagger.get_text_index_string(\n                    input_statement.in_response_to\n                )\n\n        response = self.generate_response(\n            input_statement,\n            additional_response_selection_parameters\n        )\n\n        # If streaming is enabled return the response immediately\n        if self.stream:\n            return response\n\n        # Update any response data that needs to be changed\n        if persist_values_to_response:\n            for response_key in persist_values_to_response:\n                response_value = persist_values_to_response[response_key]\n                if response_key == 'tags':\n                    input_statement.add_tags(*response_value)\n                    response.add_tags(*response_value)\n                else:\n                    setattr(input_statement, response_key, response_value)\n                    setattr(response, response_key, response_value)\n\n        if not self.read_only:\n\n            # Save the input statement\n            self.storage.create(**input_statement.serialize())\n\n            # Save the response generated for the input\n            self.learn_response(response, previous_statement=input_statement)\n\n        return response\n\n    def generate_response(self, input_statement, additional_response_selection_parameters=None):\n        \"\"\"\n        Return a response based on a given input statement.\n\n        :param input_statement: The input statement to be processed.\n        \"\"\"\n        Statement = self.storage.get_object('statement')\n\n        results = []\n        result = None\n        max_confidence = -1\n\n        for adapter in self.logic_adapters:\n            if adapter.can_process(input_statement):\n\n                output = adapter.process(input_statement, additional_response_selection_parameters)\n                results.append(output)\n\n                self.logger.info(\n                    '{} selected \"{}\" as a response with a confidence of {}'.format(\n                        adapter.class_name, output.text, output.confidence\n                    )\n                )\n\n                if output.confidence > max_confidence:\n                    result = output\n                    max_confidence = output.confidence\n            else:\n                self.logger.info(\n                    'Not processing the statement using {}'.format(adapter.class_name)\n                )\n\n        class ResultOption:\n            def __init__(self, statement, count=1):\n                self.statement = statement\n                self.count = count\n\n        # If multiple adapters agree on the same statement,\n        # then that statement is more likely to be the correct response\n        if len(results) >= 3:\n            result_options = {}\n            for result_option in results:\n                result_string = result_option.text + ':' + (result_option.in_response_to or '')\n\n                if result_string in result_options:\n                    result_options[result_string].count += 1\n                    if result_options[result_string].statement.confidence < result_option.confidence:\n                        result_options[result_string].statement = result_option\n                else:\n                    result_options[result_string] = ResultOption(\n                        result_option\n                    )\n\n            most_common = list(result_options.values())[0]\n\n            for result_option in result_options.values():\n                if result_option.count > most_common.count:\n                    most_common = result_option\n\n            self.logger.info('Selecting \"{}\" as the most common response'.format(most_common.statement.text))\n\n            if most_common.count > 1:\n                result = most_common.statement\n\n        response = Statement(\n            text=result.text,\n            in_response_to=input_statement.text,\n            conversation=input_statement.conversation,\n            persona='bot:' + self.name\n        )\n\n        response.add_tags(*result.get_tags())\n\n        response.confidence = result.confidence\n\n        return response\n\n    def learn_response(self, statement, previous_statement=None):\n        \"\"\"\n        Learn that the statement provided is a valid response.\n        \"\"\"\n        if not previous_statement:\n            previous_statement = statement.in_response_to\n\n        if not previous_statement:\n            previous_statement = self.get_latest_response(statement.conversation)\n            if previous_statement:\n                previous_statement = previous_statement.text\n\n        previous_statement_text = previous_statement\n\n        if not isinstance(previous_statement, (str, type(None), )):\n            statement.in_response_to = previous_statement.text\n        elif isinstance(previous_statement, str):\n            statement.in_response_to = previous_statement\n\n        self.logger.info('Adding \"{}\" as a response to \"{}\"'.format(\n            statement.text,\n            previous_statement_text\n        ))\n\n        if not statement.persona:\n            statement.persona = 'bot:' + self.name\n\n        # Save the response statement\n        return self.storage.create(**statement.serialize())\n\n    def get_latest_response(self, conversation: str):\n        \"\"\"\n        Returns the latest response in a conversation if it exists.\n        Returns None if a matching conversation cannot be found.\n        \"\"\"\n        conversation_statements = list(self.storage.filter(\n            conversation=conversation,\n            order_by=['id']\n        ))\n\n        # Get the most recent statement in the conversation if one exists\n        latest_statement = conversation_statements[-1] if len(conversation_statements) else None\n\n        return latest_statement\n\n    class ChatBotException(Exception):\n        pass\n"
  },
  {
    "path": "chatterbot/comparisons.py",
    "content": "\"\"\"\nThis module contains various text-comparison algorithms\ndesigned to compare one statement to another.\n\"\"\"\nfrom chatterbot.utils import get_model_for_language\nfrom difflib import SequenceMatcher\nimport spacy\n\n\nclass Comparator:\n    \"\"\"\n    Base class establishing the interface that all comparators should implement.\n    \"\"\"\n\n    def __init__(self, language):\n\n        self.language = language\n\n    def __call__(self, statement_a, statement_b):\n        return self.compare(statement_a, statement_b)\n\n    def compare_text(self, text_a: str, text_b: str) -> float:\n        \"\"\"\n        Implemented in subclasses: compare text_a to text_b.\n\n        :return: The percent of similarity between the statements based on the implemented algorithm.\n        \"\"\"\n        return 0\n\n    def compare(self, statement_a, statement_b) -> float:\n        \"\"\"\n        :return: The percent of similarity between the statements based on the implemented algorithm.\n        \"\"\"\n        return self.compare_text(statement_a.text, statement_b.text)\n\n\nclass LevenshteinDistance(Comparator):\n    \"\"\"\n    Compare two statements based on the Levenshtein distance\n    of each statement's text.\n\n    For example, there is a 65% similarity between the statements\n    \"where is the post office?\" and \"looking for the post office\"\n    based on the Levenshtein distance algorithm.\n    \"\"\"\n\n    def compare_text(self, text_a: str, text_b: str) -> float:\n        \"\"\"\n        Compare the two pieces of text.\n\n        :return: The percent of similarity between the text of the statements.\n        \"\"\"\n\n        # Return 0 if either statement has a None text value\n        if text_a is None or text_b is None:\n            return 0\n\n        # Get the lowercase version of both strings\n        statement_a_text = str(text_a.lower())\n        statement_b_text = str(text_b.lower())\n\n        similarity = SequenceMatcher(\n            None,\n            statement_a_text,\n            statement_b_text\n        )\n\n        # Calculate a decimal percent of the similarity\n        percent = round(similarity.ratio(), 2)\n\n        return percent\n\n\nclass SpacySimilarity(Comparator):\n    \"\"\"\n    Calculate the similarity of two statements using Spacy models.\n\n    NOTE:\n        You will also need to download a ``spacy`` model to use for tagging. Internally these are used to determine parts of speech for words.\n\n        The easiest way to do this is to use the ``spacy download`` command directly:\n\n        .. code-block:: python\n\n           python -m spacy download en_core_web_sm\n           python -m spacy download de_core_news_sm\n\n        Alternatively, the ``spacy`` models can be installed as Python\n        packages. The following lines could be included in a\n        ``requirements.txt`` or ``pyproject.yml`` file if you needed to pin\n        specific versions:\n\n        .. code-block:: text\n\n           https://github.com/explosion/spacy-models/releases/download/en_core_web_sm-2.3.0/en_core_web_sm-2.3.0.tar.gz#egg=en_core_web_sm\n           https://github.com/explosion/spacy-models/releases/download/de_core_news_sm-2.3.0/de_core_news_sm-2.3.0.tar.gz#egg=de_core_news_sm\n\n    \"\"\"\n\n    def __init__(self, language):\n        super().__init__(language)\n\n        model = get_model_for_language(language)\n\n        # Disable the Named Entity Recognition (NER) component because it is not necessary\n        self.nlp = spacy.load(model, exclude=['ner'])\n\n    def compare_text(self, text_a: str, text_b: str) -> float:\n        \"\"\"\n        Compare the similarity of two strings.\n\n        :return: The percent of similarity between the closest synset distance.\n        \"\"\"\n\n        # Return 0 if either statement has a None text value\n        if text_a is None or text_b is None:\n            return 0\n\n        document_a = self.nlp(text_a)\n        document_b = self.nlp(text_b)\n\n        return document_a.similarity(document_b)\n\n\nclass JaccardSimilarity(Comparator):\n    \"\"\"\n    Calculates the similarity of two statements based on the Jaccard index.\n\n    The Jaccard index is composed of a numerator and denominator.\n    In the numerator, we count the number of items that are shared between the sets.\n    In the denominator, we count the total number of items across both sets.\n    Let's say we define sentences to be equivalent if 50% or more of their tokens are equivalent.\n    Here are two sample sentences:\n\n        The young cat is hungry.\n        The cat is very hungry.\n\n    When we parse these sentences to remove stopwords, we end up with the following two sets:\n\n        {young, cat, hungry}\n        {cat, very, hungry}\n\n    In our example above, our intersection is {cat, hungry}, which has count of two.\n    The union of the sets is {young, cat, very, hungry}, which has a count of four.\n    Therefore, our `Jaccard similarity index`_ is two divided by four, or 50%.\n    Given our similarity threshold above, we would consider this to be a match.\n\n    .. _`Jaccard similarity index`: https://en.wikipedia.org/wiki/Jaccard_index\n    \"\"\"\n\n    def __init__(self, language):\n        super().__init__(language)\n\n        model = get_model_for_language(language)\n\n        # Disable the Named Entity Recognition (NER) component because it is not necessary\n        self.nlp = spacy.load(model, exclude=['ner'])\n\n    def compare_text(self, text_a: str, text_b: str) -> float:\n        \"\"\"\n        Return the calculated similarity of two\n        statements based on the Jaccard index.\n        \"\"\"\n\n        # Return 0 if either statement has a None text value\n        if text_a is None or text_b is None:\n            return 0\n\n        # Make both strings lowercase\n        document_a = self.nlp(text_a.lower())\n        document_b = self.nlp(text_b.lower())\n\n        statement_a_lemmas = frozenset([\n            token.lemma_ for token in document_a if not token.is_stop\n        ])\n        statement_b_lemmas = frozenset([\n            token.lemma_ for token in document_b if not token.is_stop\n        ])\n\n        # Calculate Jaccard similarity\n        numerator = len(statement_a_lemmas.intersection(statement_b_lemmas))\n        denominator = float(len(statement_a_lemmas.union(statement_b_lemmas)))\n        ratio = numerator / denominator\n\n        return ratio\n"
  },
  {
    "path": "chatterbot/components.py",
    "content": "\"\"\"\nCustom components for Spacy processing pipelines.\nhttps://spacy.io/usage/processing-pipelines#custom-components\n\"\"\"\nimport string\nfrom spacy.language import Language\nfrom spacy.tokens import Doc\n\n\npunctuation_table = str.maketrans(dict.fromkeys(string.punctuation))\n\n\n@Language.component('chatterbot_bigram_indexer')\ndef chatterbot_bigram_indexer(document):\n    \"\"\"\n    Generate the text string for a bigram-based search index.\n    \"\"\"\n\n    if not Doc.has_extension('search_index'):\n        Doc.set_extension('search_index', default='')\n\n    tokens = [\n        token for token in document if not (token.is_punct or token.is_stop)\n    ]\n\n    # Fall back to including stop words if needed\n    if not tokens or len(tokens) == 1:\n        tokens = [\n            token for token in document if not (token.is_punct)\n        ]\n\n    # Pairs consist of the part-of-speech of the first token and the\n    # lemma of the second token in the bigram. This provides a good\n    # balance of generalization and specificity for matching.\n    bigram_pairs = [\n        f\"{tokens[i - 1].pos_}:{tokens[i].lemma_.lower()}\"\n        for i in range(1, len(tokens))\n    ]\n\n    if not bigram_pairs:\n\n        text_without_punctuation = document.text.translate(\n            punctuation_table\n        )\n        if len(text_without_punctuation) >= 1:\n            text = text_without_punctuation.lower()\n        else:\n            text = document.text.lower()\n\n        bigram_pairs = [text]\n\n    # Assign a custom attribute at the Doc level\n    document._.search_index = ' '.join(bigram_pairs)\n\n    return document\n\n\n@Language.component('chatterbot_lowercase_indexer')\ndef chatterbot_lowercase_indexer(document):\n    \"\"\"\n    Generate the a lowercase text string for search index.\n    \"\"\"\n\n    if not Doc.has_extension('search_index'):\n        Doc.set_extension('search_index', default='')\n\n    # Assign a custom attribute at the Doc level\n    document._.search_index = document.text.lower()\n\n    return document\n"
  },
  {
    "path": "chatterbot/constants.py",
    "content": "\"\"\"\nChatterBot constants\n\"\"\"\nfrom chatterbot import languages\n\n'''\nThe maximum length of characters that the text of a statement can contain.\nThe number 1100 is used to support longer conversational statements while\nremaining within VARCHAR limits for most databases. This value should be\nenforced on a per-model basis by the data model for each storage adapter.\n'''\nSTATEMENT_TEXT_MAX_LENGTH = 1100\n\n'''\nThe maximum length of characters that the text label of a conversation can contain.\nThe number 32 was chosen because that is the length of the string representation\nof a UUID4 with no hyphens.\n'''\nCONVERSATION_LABEL_MAX_LENGTH = 32\n\n'''\nThe maximum length of text that can be stored in the persona field of the statement model.\n'''\nPERSONA_MAX_LENGTH = 50\n\n# The maximum length of characters that the name of a tag can contain\nTAG_NAME_MAX_LENGTH = 50\n\n# See other model options: https://spacy.io/models/\nDEFAULT_LANGUAGE_TO_SPACY_MODEL_MAP = {\n    languages.CAT: 'ca_core_news_sm',\n    languages.CHI: 'zh_core_web_sm',\n    languages.HRV: 'hr_core_news_sm',\n    languages.DAN: 'da_core_news_sm',\n    languages.DUT: 'nl_core_news_sm',\n    languages.ENG: 'en_core_web_sm',\n    languages.FIN: 'fi_core_news_sm',\n    languages.FRE: 'fr_core_news_sm',\n    languages.GER: 'de_core_news_sm',\n    languages.GRE: 'el_core_news_sm',\n    languages.ITA: 'it_core_news_sm',\n    languages.JPN: 'ja_core_news_sm',\n    languages.KOR: 'ko_core_news_sm',\n    languages.LIT: 'lt_core_news_sm',\n    languages.MAC: 'mk_core_news_sm',\n    languages.NOR: 'nb_core_news_sm',\n    languages.POL: 'pl_core_news_sm',\n    languages.POR: 'pt_core_news_sm',\n    languages.RUM: 'ro_core_news_sm',\n    languages.RUS: 'ru_core_news_sm',\n    languages.SLO: 'sl_core_news_sm',\n    languages.SPA: 'es_core_news_sm',\n    languages.SWE: 'sv_core_news_sm',\n    languages.UKR: 'uk_core_news_sm',\n}\n\nDEFAULT_DJANGO_APP_NAME = 'django_chatterbot'\n"
  },
  {
    "path": "chatterbot/conversation.py",
    "content": "from datetime import datetime, timezone\nfrom dateutil import parser as date_parser\n\n\nclass StatementMixin(object):\n    \"\"\"\n    This class has shared methods used to\n    normalize different statement models.\n    \"\"\"\n\n    statement_field_names = [\n        'id',\n        'text',\n        'search_text',\n        'conversation',\n        'persona',\n        'tags',\n        'in_response_to',\n        'search_in_response_to',\n        'created_at',\n    ]\n\n    extra_statement_field_names = []\n\n    def get_statement_field_names(self) -> list[str]:\n        \"\"\"\n        Return the list of field names for the statement.\n        \"\"\"\n        return self.statement_field_names + self.extra_statement_field_names\n\n    def get_tags(self) -> list[str]:\n        \"\"\"\n        Return the list of tags for this statement.\n        \"\"\"\n        return self.tags\n\n    def add_tags(self, *tags):\n        \"\"\"\n        Add a list of strings to the statement as tags.\n        \"\"\"\n        self.tags.extend(tags)\n\n    def serialize(self) -> dict:\n        \"\"\"\n        :returns: A dictionary representation of the statement object.\n        \"\"\"\n        data = {}\n\n        for field_name in self.get_statement_field_names():\n            format_method = getattr(self, 'get_{}'.format(\n                field_name\n            ), None)\n\n            if format_method:\n                data[field_name] = format_method()\n            else:\n                data[field_name] = getattr(self, field_name)\n\n        return data\n\n\nclass Statement(StatementMixin):\n    \"\"\"\n    A statement represents a single spoken entity, sentence or\n    phrase that someone can say.\n    \"\"\"\n\n    __slots__ = (\n        'id',\n        'text',\n        'search_text',\n        'conversation',\n        'persona',\n        'tags',\n        'in_response_to',\n        'search_in_response_to',\n        'created_at',\n        'confidence',\n        'storage',\n    )\n\n    def __init__(self, text: str, in_response_to=None, **kwargs):\n\n        self.id = kwargs.get('id')\n        self.text = str(text)\n        self.search_text = kwargs.get('search_text', '')\n        self.conversation = kwargs.get('conversation', '')\n        self.persona = kwargs.get('persona', '')\n        self.tags = kwargs.pop('tags', [])\n        self.in_response_to = in_response_to\n        self.search_in_response_to = kwargs.get('search_in_response_to', '')\n        self.created_at = kwargs.get('created_at', datetime.now())\n\n        if not isinstance(self.created_at, datetime):\n            self.created_at = date_parser.parse(self.created_at)\n\n        # Set timezone to UTC if no timezone was provided\n        if not self.created_at.tzinfo:\n            self.created_at = self.created_at.replace(tzinfo=timezone.utc)\n\n        # This is the confidence with which the chat bot believes\n        # this is an accurate response. This value is set when the\n        # statement is returned by the chat bot.\n        self.confidence = kwargs.get('confidence', 0)\n\n        self.storage = None\n\n    def __str__(self):\n        return self.text\n\n    def __repr__(self):\n        return '<Statement text:%s>' % (self.text)\n\n    def save(self):\n        \"\"\"\n        Save the statement in the database.\n        \"\"\"\n        self.storage.update(self)\n"
  },
  {
    "path": "chatterbot/corpus.py",
    "content": "import os\nimport io\nimport glob\nfrom pathlib import Path\nfrom chatterbot.exceptions import OptionalDependencyImportError\n\ntry:\n    from chatterbot_corpus.corpus import DATA_DIRECTORY\nexcept (ImportError, ModuleNotFoundError):\n    # Default to the home directory of the current user\n    DATA_DIRECTORY = os.path.join(\n        Path.home(),\n        'chatterbot_corpus',\n        'data'\n    )\n\n\nCORPUS_EXTENSION = 'yml'\n\n\ndef get_file_path(dotted_path, extension='json') -> str:\n    \"\"\"\n    Reads a dotted file path and returns the file path.\n    \"\"\"\n    # If the operating system's file path seperator character is in the string\n    if os.sep in dotted_path or '/' in dotted_path:\n        # Assume the path is a valid file path\n        return dotted_path\n\n    parts = dotted_path.split('.')\n    if parts[0] == 'chatterbot':\n        parts.pop(0)\n        parts[0] = DATA_DIRECTORY\n\n    corpus_path = os.path.join(*parts)\n\n    path_with_extension = '{}.{}'.format(corpus_path, extension)\n    if os.path.exists(path_with_extension):\n        corpus_path = path_with_extension\n\n    return corpus_path\n\n\ndef read_corpus(file_name) -> dict:\n    \"\"\"\n    Read and return the data from a corpus json file.\n    \"\"\"\n    try:\n        import yaml\n    except ImportError:\n        message = (\n            'Unable to import \"yaml\".\\n'\n            'Please install \"pyyaml\" to enable chatterbot corpus functionality:\\n'\n            'pip install pyyaml'\n        )\n        raise OptionalDependencyImportError(message)\n\n    with io.open(file_name, encoding='utf-8') as data_file:\n        return yaml.safe_load(data_file)\n\n\ndef list_corpus_files(dotted_path) -> list[str]:\n    \"\"\"\n    Return a list of file paths to each data file in the specified corpus.\n    \"\"\"\n    corpus_path = get_file_path(dotted_path, extension=CORPUS_EXTENSION)\n    paths = []\n\n    if os.path.isdir(corpus_path):\n        paths = glob.glob(corpus_path + '/**/*.' + CORPUS_EXTENSION, recursive=True)\n    else:\n        paths.append(corpus_path)\n\n    paths.sort()\n    return paths\n\n\ndef load_corpus(*data_file_paths):\n    \"\"\"\n    Return the data contained within a specified corpus.\n    \"\"\"\n    for file_path in data_file_paths:\n        corpus = []\n        corpus_data = read_corpus(file_path)\n\n        conversations = corpus_data.get('conversations', [])\n        corpus.extend(conversations)\n\n        categories = corpus_data.get('categories', [])\n\n        yield corpus, categories, file_path\n"
  },
  {
    "path": "chatterbot/exceptions.py",
    "content": "class OptionalDependencyImportError(ImportError):\n    \"\"\"\n    An exception raised when a feature requires an optional dependency to be installed.\n    \"\"\"\n    pass\n"
  },
  {
    "path": "chatterbot/ext/__init__.py",
    "content": ""
  },
  {
    "path": "chatterbot/ext/django_chatterbot/__init__.py",
    "content": "default_app_config = (\n    'chatterbot.ext.django_chatterbot.apps.DjangoChatterBotConfig'\n)\n"
  },
  {
    "path": "chatterbot/ext/django_chatterbot/abstract_models.py",
    "content": "from chatterbot.conversation import StatementMixin\nfrom chatterbot import constants\nfrom django.db import models\nfrom django.utils import timezone\nfrom django.conf import settings\nfrom django.apps import apps\n\n\nDJANGO_APP_NAME = constants.DEFAULT_DJANGO_APP_NAME\n\n# Default model paths for swappable models\n# These can be overridden via CHATTERBOT_STATEMENT_MODEL and CHATTERBOT_TAG_MODEL settings\nDEFAULT_STATEMENT_MODEL = f'{DJANGO_APP_NAME}.Statement'\nDEFAULT_TAG_MODEL = f'{DJANGO_APP_NAME}.Tag'\n\n\nclass AbstractBaseTag(models.Model):\n    \"\"\"\n    The abstract base tag allows other models to be created\n    using the attributes that exist on the default models.\n    \"\"\"\n\n    name = models.SlugField(\n        max_length=constants.TAG_NAME_MAX_LENGTH,\n        unique=True,\n        help_text='The unique name of the tag.'\n    )\n\n    class Meta:\n        abstract = True\n\n    def __str__(self):\n        return self.name\n\n\nclass AbstractBaseStatement(models.Model, StatementMixin):\n    \"\"\"\n    The abstract base statement allows other models to be created\n    using the attributes that exist on the default models.\n    \"\"\"\n\n    text = models.CharField(\n        max_length=constants.STATEMENT_TEXT_MAX_LENGTH,\n        help_text='The text of the statement.'\n    )\n\n    search_text = models.CharField(\n        max_length=constants.STATEMENT_TEXT_MAX_LENGTH,\n        blank=True,\n        help_text='A modified version of the statement text optimized for searching.'\n    )\n\n    conversation = models.CharField(\n        max_length=constants.CONVERSATION_LABEL_MAX_LENGTH,\n        help_text='A label used to link this statement to a conversation.'\n    )\n\n    created_at = models.DateTimeField(\n        default=timezone.now,\n        help_text='The date and time that the statement was created at.'\n    )\n\n    in_response_to = models.CharField(\n        max_length=constants.STATEMENT_TEXT_MAX_LENGTH,\n        null=True,\n        help_text='The text of the statement that this statement is in response to.'\n    )\n\n    search_in_response_to = models.CharField(\n        max_length=constants.STATEMENT_TEXT_MAX_LENGTH,\n        blank=True,\n        help_text='A modified version of the in_response_to text optimized for searching.'\n    )\n\n    persona = models.CharField(\n        max_length=constants.PERSONA_MAX_LENGTH,\n        help_text='A label used to link this statement to a persona.'\n    )\n\n    tags = models.ManyToManyField(\n        settings.CHATTERBOT_TAG_MODEL if hasattr(\n            settings, 'CHATTERBOT_TAG_MODEL'\n        ) else DEFAULT_TAG_MODEL,\n        related_name='statements',\n        help_text='The tags that are associated with this statement.'\n    )\n\n    # This is the confidence with which the chat bot believes\n    # this is an accurate response. This value is set when the\n    # statement is returned by the chat bot.\n    confidence = 0\n\n    class Meta:\n        abstract = True\n        indexes = [\n            models.Index(\n                fields=['search_text'],\n                name='idx_cb_search_text'\n            ),\n            models.Index(\n                fields=['search_in_response_to'], name='idx_cb_search_in_response_to'\n            ),\n        ]\n\n    def __str__(self):\n        if len(self.text.strip()) > 60:\n            return '{}...'.format(self.text[:57])\n        elif len(self.text.strip()) > 0:\n            return self.text\n        return '<empty>'\n\n    @classmethod\n    def get_tag_model(cls):\n        \"\"\"\n        Return the Tag model class, respecting the swappable setting.\n\n        This method checks:\n        1. Django settings (CHATTERBOT_TAG_MODEL) - project-wide configuration\n        2. The model referenced by the 'tags' field - handles custom models via kwargs\n        3. Falls back to DEFAULT_TAG_MODEL if introspection fails\n\n        This ensures the correct Tag model is used even when custom models\n        are specified via storage adapter kwargs rather than Django settings.\n        \"\"\"\n        tag_model_path = getattr(settings, 'CHATTERBOT_TAG_MODEL', None)\n\n        if tag_model_path:\n            return apps.get_model(tag_model_path)\n\n        # If no setting, infer from the ManyToManyField relationship for\n        # cases where custom models are specified via kwargs\n        try:\n            # Get the model that this class's 'tags' field points to\n            tags_field = cls._meta.get_field('tags')\n            related_model = tags_field.related_model\n\n            # Resolve strings (lazy references)\n            if isinstance(related_model, str):\n                return apps.get_model(related_model)\n            return related_model\n        except Exception:\n            # Fallback to default if introspection fails\n            return apps.get_model(DEFAULT_TAG_MODEL)\n\n    def get_tags(self) -> list[str]:\n        \"\"\"\n        Return the list of tags for this statement.\n        \"\"\"\n        return list(self.tags.values_list('name', flat=True))\n\n    def add_tags(self, *tags):\n        \"\"\"\n        Add a list of strings to the statement as tags.\n        \"\"\"\n        TagModel = self.get_tag_model()\n\n        for tag_name in tags:\n            tag_obj, _created = TagModel.objects.get_or_create(name=tag_name)\n            self.tags.add(tag_obj)\n"
  },
  {
    "path": "chatterbot/ext/django_chatterbot/admin.py",
    "content": "from django.contrib import admin\nfrom chatterbot.ext.django_chatterbot.model_admin import StatementAdmin, TagAdmin\nfrom chatterbot.ext.django_chatterbot.models import Statement, Tag\n\n\nadmin.site.register(Statement, StatementAdmin)\nadmin.site.register(Tag, TagAdmin)\n"
  },
  {
    "path": "chatterbot/ext/django_chatterbot/apps.py",
    "content": "from django.apps import AppConfig\n\n\nclass DjangoChatterBotConfig(AppConfig):\n\n    name = 'chatterbot.ext.django_chatterbot'\n    label = 'django_chatterbot'\n    verbose_name = 'Django ChatterBot'\n\n    def ready(self):\n        from chatterbot.ext.django_chatterbot import settings as defaults\n        from django.conf import settings\n\n        settings.CHATTERBOT = getattr(settings, 'CHATTERBOT', {})\n        settings.CHATTERBOT.update(defaults.CHATTERBOT_DEFAULTS)\n"
  },
  {
    "path": "chatterbot/ext/django_chatterbot/migrations/0001_initial.py",
    "content": "from django.db import migrations, models\nimport django.db.models.deletion\n\n\nclass Migration(migrations.Migration):\n\n    initial = True\n\n    dependencies = []\n\n    operations = [\n        migrations.CreateModel(\n            name='Response',\n            fields=[\n                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),\n                ('occurrence', models.PositiveIntegerField(default=0)),\n            ],\n        ),\n        migrations.CreateModel(\n            name='Statement',\n            fields=[\n                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),\n                ('text', models.CharField(max_length=255, unique=True)),\n            ],\n        ),\n        migrations.AddField(\n            model_name='response',\n            name='response',\n            field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='+', to='django_chatterbot.Statement'),\n        ),\n        migrations.AddField(\n            model_name='response',\n            name='statement',\n            field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='in_response_to', to='django_chatterbot.Statement'),\n        ),\n    ]\n"
  },
  {
    "path": "chatterbot/ext/django_chatterbot/migrations/0002_statement_extra_data.py",
    "content": "# Generated by Django 1.10.2 on 2016-10-30 12:13\nfrom django.db import migrations, models\n\n\nclass Migration(migrations.Migration):\n\n    dependencies = [\n        ('django_chatterbot', '0001_initial'),\n    ]\n\n    operations = [\n        migrations.AddField(\n            model_name='statement',\n            name='extra_data',\n            field=models.CharField(default='{}', max_length=500),\n            preserve_default=False,\n        ),\n    ]\n"
  },
  {
    "path": "chatterbot/ext/django_chatterbot/migrations/0003_change_occurrence_default.py",
    "content": "# Generated by Django 1.9 on 2016-12-12 00:06\nfrom django.db import migrations, models\n\n\nclass Migration(migrations.Migration):\n\n    dependencies = [\n        ('django_chatterbot', '0002_statement_extra_data'),\n    ]\n\n    operations = [\n        migrations.AlterField(\n            model_name='response',\n            name='occurrence',\n            field=models.PositiveIntegerField(default=1),\n        ),\n    ]\n"
  },
  {
    "path": "chatterbot/ext/django_chatterbot/migrations/0004_rename_in_response_to.py",
    "content": "# Generated by Django 1.10.3 on 2016-12-04 23:52\nfrom django.db import migrations, models\nimport django.db.models.deletion\n\n\nclass Migration(migrations.Migration):\n\n    dependencies = [\n        ('django_chatterbot', '0003_change_occurrence_default'),\n    ]\n\n    operations = [\n        migrations.AlterField(\n            model_name='response',\n            name='statement',\n            field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='in_response', to='django_chatterbot.Statement'),\n        ),\n        migrations.AlterField(\n            model_name='response',\n            name='response',\n            field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='responses', to='django_chatterbot.Statement'),\n        ),\n    ]\n"
  },
  {
    "path": "chatterbot/ext/django_chatterbot/migrations/0005_statement_created_at.py",
    "content": "# Generated by Django 1.10.1 on 2016-12-29 19:20\nfrom django.db import migrations, models\nimport django.utils.timezone\n\n\nclass Migration(migrations.Migration):\n\n    dependencies = [\n        ('django_chatterbot', '0004_rename_in_response_to'),\n    ]\n\n    operations = [\n        migrations.AddField(\n            model_name='statement',\n            name='created_at',\n            field=models.DateTimeField(\n                default=django.utils.timezone.now,\n                help_text='The date and time that this statement was created at.'\n            ),\n        ),\n    ]\n"
  },
  {
    "path": "chatterbot/ext/django_chatterbot/migrations/0006_create_conversation.py",
    "content": "# Generated by Django 1.9 on 2017-01-17 07:02\nfrom django.db import migrations, models\nimport django.db.models.deletion\nimport django.utils.timezone\n\n\nclass Migration(migrations.Migration):\n\n    dependencies = [\n        ('django_chatterbot', '0005_statement_created_at'),\n    ]\n\n    operations = [\n        migrations.CreateModel(\n            name='Conversation',\n            fields=[\n                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),\n            ],\n        ),\n        migrations.AlterField(\n            model_name='statement',\n            name='created_at',\n            field=models.DateTimeField(default=django.utils.timezone.now, help_text='The date and time that this statement was created at.'),\n        ),\n        migrations.AddField(\n            model_name='conversation',\n            name='statements',\n            field=models.ManyToManyField(help_text='The statements in this conversation.', related_name='conversation', to='django_chatterbot.Statement'),\n        ),\n    ]\n"
  },
  {
    "path": "chatterbot/ext/django_chatterbot/migrations/0007_response_created_at.py",
    "content": "# Generated by Django 1.11 on 2017-07-18 00:16\nfrom django.db import migrations, models\nimport django.utils.timezone\n\n\nclass Migration(migrations.Migration):\n\n    dependencies = [\n        ('django_chatterbot', '0006_create_conversation'),\n    ]\n\n    operations = [\n        migrations.AddField(\n            model_name='response',\n            name='created_at',\n            field=models.DateTimeField(\n                default=django.utils.timezone.now,\n                help_text='The date and time that this response was created at.'\n            ),\n        ),\n    ]\n"
  },
  {
    "path": "chatterbot/ext/django_chatterbot/migrations/0008_update_conversations.py",
    "content": "# Generated by Django 1.11 on 2017-07-18 11:25\nfrom django.db import migrations, models\n\n\nclass Migration(migrations.Migration):\n\n    dependencies = [\n        ('django_chatterbot', '0007_response_created_at'),\n    ]\n\n    operations = [\n        migrations.RemoveField(\n            model_name='conversation',\n            name='statements',\n        ),\n        migrations.RemoveField(\n            model_name='response',\n            name='occurrence',\n        ),\n        migrations.RemoveField(\n            model_name='statement',\n            name='created_at',\n        ),\n        migrations.AddField(\n            model_name='conversation',\n            name='responses',\n            field=models.ManyToManyField(help_text='The responses in this conversation.', related_name='conversations', to='django_chatterbot.Response'),\n        ),\n    ]\n"
  },
  {
    "path": "chatterbot/ext/django_chatterbot/migrations/0009_tags.py",
    "content": "# Generated by Django 1.11a1 on 2017-07-07 00:12\nfrom django.db import migrations, models\n\n\nclass Migration(migrations.Migration):\n\n    dependencies = [\n        ('django_chatterbot', '0008_update_conversations'),\n    ]\n\n    operations = [\n        migrations.CreateModel(\n            name='Tag',\n            fields=[\n                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),\n                ('name', models.SlugField()),\n            ],\n            options={\n                'abstract': False,\n            },\n        ),\n        migrations.AlterField(\n            model_name='statement',\n            name='text',\n            field=models.CharField(max_length=255, unique=True),\n        ),\n        migrations.AddField(\n            model_name='tag',\n            name='statements',\n            field=models.ManyToManyField(related_name='tags', to='django_chatterbot.Statement'),\n        ),\n    ]\n"
  },
  {
    "path": "chatterbot/ext/django_chatterbot/migrations/0010_statement_text.py",
    "content": "# Generated by Django 1.11.4 on 2017-08-16 00:56\nfrom django.db import migrations, models\n\n\nclass Migration(migrations.Migration):\n\n    dependencies = [\n        ('django_chatterbot', '0009_tags'),\n    ]\n\n    operations = [\n        migrations.AlterField(\n            model_name='statement',\n            name='text',\n            field=models.CharField(max_length=400, unique=True),\n        ),\n    ]\n"
  },
  {
    "path": "chatterbot/ext/django_chatterbot/migrations/0011_blank_extra_data.py",
    "content": "# Generated by Django 1.11.4 on 2017-08-20 13:55\nfrom django.db import migrations, models\n\n\nclass Migration(migrations.Migration):\n\n    dependencies = [\n        ('django_chatterbot', '0010_statement_text'),\n    ]\n\n    operations = [\n        migrations.AlterField(\n            model_name='statement',\n            name='extra_data',\n            field=models.CharField(blank=True, max_length=500),\n        ),\n    ]\n"
  },
  {
    "path": "chatterbot/ext/django_chatterbot/migrations/0012_statement_created_at.py",
    "content": "from django.db import migrations, models\nimport django.utils.timezone\n\n\nclass Migration(migrations.Migration):\n\n    dependencies = [\n        ('django_chatterbot', '0011_blank_extra_data'),\n    ]\n\n    operations = [\n        migrations.AddField(\n            model_name='statement',\n            name='created_at',\n            field=models.DateTimeField(\n                default=django.utils.timezone.now,\n                help_text='The date and time that the statement was created at.'\n            ),\n        ),\n    ]\n"
  },
  {
    "path": "chatterbot/ext/django_chatterbot/migrations/0013_change_conversations.py",
    "content": "# Generated by Django 1.11 on 2018-09-13 01:01\nfrom django.db import migrations, models\n\n\nclass Migration(migrations.Migration):\n\n    dependencies = [\n        ('django_chatterbot', '0012_statement_created_at'),\n    ]\n\n    operations = [\n        migrations.RemoveField(\n            model_name='conversation',\n            name='responses',\n        ),\n        migrations.RemoveField(\n            model_name='response',\n            name='response',\n        ),\n        migrations.RemoveField(\n            model_name='response',\n            name='statement',\n        ),\n        migrations.AddField(\n            model_name='statement',\n            name='conversation',\n            field=models.CharField(default='default', max_length=32),\n            preserve_default=False,\n        ),\n        migrations.AddField(\n            model_name='statement',\n            name='in_response_to',\n            field=models.CharField(max_length=400, null=True),\n        ),\n        migrations.AlterField(\n            model_name='statement',\n            name='text',\n            field=models.CharField(max_length=400),\n        ),\n        migrations.DeleteModel(\n            name='Conversation',\n        ),\n        migrations.DeleteModel(\n            name='Response',\n        ),\n    ]\n"
  },
  {
    "path": "chatterbot/ext/django_chatterbot/migrations/0014_remove_statement_extra_data.py",
    "content": "from django.db import migrations\n\n\nclass Migration(migrations.Migration):\n\n    dependencies = [\n        ('django_chatterbot', '0013_change_conversations'),\n    ]\n\n    operations = [\n        migrations.RemoveField(\n            model_name='statement',\n            name='extra_data',\n        ),\n    ]\n"
  },
  {
    "path": "chatterbot/ext/django_chatterbot/migrations/0015_statement_persona.py",
    "content": "from django.db import migrations, models\n\n\nclass Migration(migrations.Migration):\n\n    dependencies = [\n        ('django_chatterbot', '0014_remove_statement_extra_data'),\n    ]\n\n    operations = [\n        migrations.AddField(\n            model_name='statement',\n            name='persona',\n            field=models.CharField(default='', max_length=50),\n            preserve_default=False,\n        ),\n    ]\n"
  },
  {
    "path": "chatterbot/ext/django_chatterbot/migrations/0016_statement_stemmed_text.py",
    "content": "from django.db import migrations, models\n\n\nclass Migration(migrations.Migration):\n\n    dependencies = [\n        ('django_chatterbot', '0015_statement_persona'),\n    ]\n\n    operations = [\n        migrations.AddField(\n            model_name='statement',\n            name='search_text',\n            field=models.CharField(blank=True, max_length=400),\n        ),\n        migrations.AddField(\n            model_name='statement',\n            name='search_in_response_to',\n            field=models.CharField(blank=True, max_length=400),\n        ),\n    ]\n"
  },
  {
    "path": "chatterbot/ext/django_chatterbot/migrations/0017_tags_unique.py",
    "content": "from django.db import migrations, models\n\n\nclass Migration(migrations.Migration):\n\n    dependencies = [\n        ('django_chatterbot', '0016_statement_stemmed_text'),\n    ]\n\n    operations = [\n        migrations.RemoveField(\n            model_name='tag',\n            name='statements',\n        ),\n        migrations.AddField(\n            model_name='statement',\n            name='tags',\n            field=models.ManyToManyField(\n                related_name='statements',\n                to='django_chatterbot.Tag'\n            ),\n        ),\n        migrations.AlterField(\n            model_name='tag',\n            name='name',\n            field=models.SlugField(unique=True),\n        ),\n    ]\n"
  },
  {
    "path": "chatterbot/ext/django_chatterbot/migrations/0018_text_max_length.py",
    "content": "from django.db import migrations, models\n\n\nclass Migration(migrations.Migration):\n\n    dependencies = [\n        ('django_chatterbot', '0017_tags_unique'),\n    ]\n\n    operations = [\n        migrations.AlterField(\n            model_name='statement',\n            name='in_response_to',\n            field=models.CharField(max_length=255, null=True),\n        ),\n        migrations.AlterField(\n            model_name='statement',\n            name='search_in_response_to',\n            field=models.CharField(blank=True, max_length=255),\n        ),\n        migrations.AlterField(\n            model_name='statement',\n            name='search_text',\n            field=models.CharField(blank=True, max_length=255),\n        ),\n        migrations.AlterField(\n            model_name='statement',\n            name='text',\n            field=models.CharField(max_length=255),\n        ),\n    ]\n"
  },
  {
    "path": "chatterbot/ext/django_chatterbot/migrations/0019_alter_statement_id_alter_tag_id_and_more.py",
    "content": "# Generated by Django 4.2.19 on 2025-02-09 13:57\n\nfrom django.db import migrations, models\n\n\nclass Migration(migrations.Migration):\n\n    dependencies = [\n        ('django_chatterbot', '0018_text_max_length'),\n    ]\n\n    operations = [\n        migrations.AlterField(\n            model_name='statement',\n            name='id',\n            field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),\n        ),\n        migrations.AlterField(\n            model_name='tag',\n            name='id',\n            field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),\n        ),\n        migrations.AddIndex(\n            model_name='statement',\n            index=models.Index(fields=['search_text'], name='idx_cb_search_text'),\n        ),\n        migrations.AddIndex(\n            model_name='statement',\n            index=models.Index(fields=['search_in_response_to'], name='idx_cb_search_in_response_to'),\n        ),\n    ]\n"
  },
  {
    "path": "chatterbot/ext/django_chatterbot/migrations/0020_alter_statement_conversation_and_more.py",
    "content": "# Generated by Django 4.1 on 2025-03-29 23:27\n\nfrom django.db import migrations, models\n\n\nclass Migration(migrations.Migration):\n\n    dependencies = [\n        ('django_chatterbot', '0019_alter_statement_id_alter_tag_id_and_more'),\n    ]\n\n    operations = [\n        migrations.AlterField(\n            model_name='statement',\n            name='conversation',\n            field=models.CharField(help_text='A label used to link this statement to a conversation.', max_length=32),\n        ),\n        migrations.AlterField(\n            model_name='statement',\n            name='in_response_to',\n            field=models.CharField(help_text='The text of the statement that this statement is in response to.', max_length=255, null=True),\n        ),\n        migrations.AlterField(\n            model_name='statement',\n            name='persona',\n            field=models.CharField(help_text='A label used to link this statement to a persona.', max_length=50),\n        ),\n        migrations.AlterField(\n            model_name='statement',\n            name='search_in_response_to',\n            field=models.CharField(blank=True, help_text='A modified version of the in_response_to text optimized for searching.', max_length=255),\n        ),\n        migrations.AlterField(\n            model_name='statement',\n            name='search_text',\n            field=models.CharField(blank=True, help_text='A modified version of the statement text optimized for searching.', max_length=255),\n        ),\n        migrations.AlterField(\n            model_name='statement',\n            name='tags',\n            field=models.ManyToManyField(help_text='The tags that are associated with this statement.', related_name='statements', to='django_chatterbot.tag'),\n        ),\n        migrations.AlterField(\n            model_name='statement',\n            name='text',\n            field=models.CharField(help_text='The text of the statement.', max_length=255),\n        ),\n        migrations.AlterField(\n            model_name='tag',\n            name='name',\n            field=models.SlugField(help_text='The unique name of the tag.', unique=True),\n        ),\n    ]\n"
  },
  {
    "path": "chatterbot/ext/django_chatterbot/migrations/0021_increase_text_max_length_to_1100.py",
    "content": "\"\"\"\nDjango migration to increase text field max_length from 255 to 1100.\n\nThis migration alters all text-related fields in the Statement model:\n- text\n- search_text\n- in_response_to\n- search_in_response_to\n\nThis change supports longer conversational statements while remaining\nwithin VARCHAR limits for most databases.\n\"\"\"\nfrom django.db import migrations, models\n\n\nclass Migration(migrations.Migration):\n\n    dependencies = [\n        ('django_chatterbot', '0020_alter_statement_conversation_and_more'),\n    ]\n\n    operations = [\n        migrations.AlterField(\n            model_name='statement',\n            name='text',\n            field=models.CharField(max_length=1100, help_text='The text of the statement.'),\n        ),\n        migrations.AlterField(\n            model_name='statement',\n            name='search_text',\n            field=models.CharField(\n                blank=True,\n                max_length=1100,\n                help_text='A modified version of the statement text optimized for searching.'\n            ),\n        ),\n        migrations.AlterField(\n            model_name='statement',\n            name='in_response_to',\n            field=models.CharField(\n                max_length=1100,\n                null=True,\n                help_text='The text of the statement that this statement is in response to.'\n            ),\n        ),\n        migrations.AlterField(\n            model_name='statement',\n            name='search_in_response_to',\n            field=models.CharField(\n                blank=True,\n                max_length=1100,\n                help_text='A modified version of the in_response_to text optimized for searching.'\n            ),\n        ),\n    ]\n"
  },
  {
    "path": "chatterbot/ext/django_chatterbot/migrations/__init__.py",
    "content": ""
  },
  {
    "path": "chatterbot/ext/django_chatterbot/model_admin.py",
    "content": "from django.contrib import admin\n\n\nclass StatementAdmin(admin.ModelAdmin):\n    list_display = ('text', 'in_response_to', 'conversation', 'created_at', )\n    list_filter = ('text', 'created_at', )\n    search_fields = ('text', )\n\n\nclass TagAdmin(admin.ModelAdmin):\n    list_display = ('name', )\n    list_filter = ('name', )\n    search_fields = ('name', )\n"
  },
  {
    "path": "chatterbot/ext/django_chatterbot/models.py",
    "content": "from chatterbot.ext.django_chatterbot.abstract_models import AbstractBaseStatement, AbstractBaseTag\n\n\nclass Statement(AbstractBaseStatement):\n    \"\"\"\n    A statement represents a single spoken entity, sentence or\n    phrase that someone can say.\n\n    This model can be swapped for a custom model by setting\n    CHATTERBOT_STATEMENT_MODEL in your Django settings.\n    \"\"\"\n\n    class Meta:\n        swappable = 'CHATTERBOT_STATEMENT_MODEL'\n\n\nclass Tag(AbstractBaseTag):\n    \"\"\"\n    A label that categorizes a statement.\n\n    This model can be swapped for a custom model by setting\n    CHATTERBOT_TAG_MODEL in your Django settings.\n    \"\"\"\n\n    class Meta:\n        swappable = 'CHATTERBOT_TAG_MODEL'\n"
  },
  {
    "path": "chatterbot/ext/django_chatterbot/settings.py",
    "content": "\"\"\"\nDefault ChatterBot settings for Django.\n\"\"\"\nfrom django.conf import settings\nfrom chatterbot import constants\n\n\nCHATTERBOT = getattr(settings, 'CHATTERBOT', {})\n\nCHATTERBOT_DEFAULTS = {\n    'name': 'ChatterBot',\n    'storage_adapter': 'chatterbot.storage.DjangoStorageAdapter',\n    'django_app_name': constants.DEFAULT_DJANGO_APP_NAME\n}\n\nCHATTERBOT.update(CHATTERBOT_DEFAULTS)\n"
  },
  {
    "path": "chatterbot/ext/sqlalchemy_app/__init__.py",
    "content": ""
  },
  {
    "path": "chatterbot/ext/sqlalchemy_app/models.py",
    "content": "from sqlalchemy import Table, Column, Integer, String, DateTime, ForeignKey\nfrom sqlalchemy.orm import relationship, declarative_base\nfrom sqlalchemy.sql import func\nfrom sqlalchemy.ext.declarative import declared_attr\n\nfrom chatterbot.conversation import StatementMixin\nfrom chatterbot import constants\n\n\nclass ModelBase(object):\n    \"\"\"\n    An augmented base class for SqlAlchemy models.\n    \"\"\"\n\n    @declared_attr\n    def __tablename__(cls) -> str:\n        \"\"\"\n        Return the lowercase class name as the name of the table.\n        \"\"\"\n        return cls.__name__.lower()\n\n    id = Column(\n        Integer,\n        primary_key=True,\n        autoincrement=True\n    )\n\n\nBase = declarative_base(cls=ModelBase)\n\n\ntag_association_table = Table(\n    'tag_association',\n    Base.metadata,\n    Column('tag_id', Integer, ForeignKey('tag.id')),\n    Column('statement_id', Integer, ForeignKey('statement.id'))\n)\n\n\nclass Tag(Base):\n    \"\"\"\n    A tag that describes a statement.\n    \"\"\"\n\n    name = Column(\n        String(constants.TAG_NAME_MAX_LENGTH),\n        unique=True\n    )\n\n\nclass Statement(Base, StatementMixin):\n    \"\"\"\n    A Statement represents a sentence or phrase.\n    \"\"\"\n\n    confidence = 0\n\n    text = Column(\n        String(constants.STATEMENT_TEXT_MAX_LENGTH)\n    )\n\n    search_text = Column(\n        String(constants.STATEMENT_TEXT_MAX_LENGTH),\n        nullable=False,\n        server_default=''\n    )\n\n    conversation = Column(\n        String(constants.CONVERSATION_LABEL_MAX_LENGTH),\n        nullable=False,\n        server_default=''\n    )\n\n    created_at = Column(\n        DateTime(timezone=True),\n        server_default=func.now()\n    )\n\n    tags = relationship(\n        'Tag',\n        secondary=lambda: tag_association_table,\n        backref='statements'\n    )\n\n    in_response_to = Column(\n        String(constants.STATEMENT_TEXT_MAX_LENGTH),\n        nullable=True\n    )\n\n    search_in_response_to = Column(\n        String(constants.STATEMENT_TEXT_MAX_LENGTH),\n        nullable=False,\n        server_default=''\n    )\n\n    persona = Column(\n        String(constants.PERSONA_MAX_LENGTH),\n        nullable=False,\n        server_default=''\n    )\n\n    def get_tags(self) -> list[str]:\n        \"\"\"\n        Return a list of tags for this statement.\n        \"\"\"\n        return [tag.name for tag in self.tags]\n\n    def add_tags(self, *tags):\n        \"\"\"\n        Add a list of strings to the statement as tags.\n        \"\"\"\n        self.tags.extend([\n            Tag(name=tag) for tag in tags\n        ])\n"
  },
  {
    "path": "chatterbot/filters.py",
    "content": "def get_recent_repeated_responses(chatbot, conversation, sample=10, threshold=3, quantity=3) -> list:\n    \"\"\"\n    A filter that eliminates possibly repetitive responses to prevent\n    a chat bot from repeating statements that it has recently said.\n    \"\"\"\n    from collections import Counter\n\n    # Get the most recent statements from the conversation\n    conversation_statements = list(chatbot.storage.filter(\n        conversation=conversation,\n        order_by=['id']\n    ))[sample * -1:]\n\n    text_of_recent_responses = [\n        statement.text for statement in conversation_statements\n    ]\n\n    counter = Counter(text_of_recent_responses)\n\n    # Find the n most common responses from the conversation\n    most_common = counter.most_common(quantity)\n\n    return [\n        counted[0] for counted in most_common\n        if counted[1] >= threshold\n    ]\n"
  },
  {
    "path": "chatterbot/languages.py",
    "content": "import sys\nimport inspect\n\n\nclass AAR:\n    ISO_639_1 = ''\n    ISO_639 = 'aar'\n    ENGLISH_NAME = 'Afar'\n\n\nclass ABK:\n    ISO_639_1 = ''\n    ISO_639 = 'abk'\n    ENGLISH_NAME = 'Abkhazian'\n\n\nclass ACE:\n    ISO_639_1 = ''\n    ISO_639 = 'ace'\n    ENGLISH_NAME = 'Achinese'\n\n\nclass ACH:\n    ISO_639_1 = ''\n    ISO_639 = 'ach'\n    ENGLISH_NAME = 'Acoli'\n\n\nclass ADA:\n    ISO_639_1 = ''\n    ISO_639 = 'ada'\n    ENGLISH_NAME = 'Adangme'\n\n\nclass ADY:\n    ISO_639_1 = ''\n    ISO_639 = 'ady'\n    ENGLISH_NAME = 'Adyghe'\n\n\nclass AFH:\n    ISO_639_1 = ''\n    ISO_639 = 'afh'\n    ENGLISH_NAME = 'Afrihili'\n\n\nclass AFR:\n    ISO_639_1 = ''\n    ISO_639 = 'afr'\n    ENGLISH_NAME = 'Afrikaans'\n\n\nclass AIN:\n    ISO_639_1 = ''\n    ISO_639 = 'ain'\n    ENGLISH_NAME = 'Ainu'\n\n\nclass AKA:\n    ISO_639_1 = ''\n    ISO_639 = 'aka'\n    ENGLISH_NAME = 'Akan'\n\n\nclass AKK:\n    ISO_639_1 = ''\n    ISO_639 = 'akk'\n    ENGLISH_NAME = 'Akkadian'\n\n\nclass ALB:\n    ISO_639_1 = ''\n    ISO_639 = 'alb'\n    ENGLISH_NAME = 'Albanian'\n\n\nclass ALE:\n    ISO_639_1 = ''\n    ISO_639 = 'ale'\n    ENGLISH_NAME = 'Aleut'\n\n\nclass ALT:\n    ISO_639_1 = ''\n    ISO_639 = 'alt'\n    ENGLISH_NAME = 'SouthernAltai'\n\n\nclass AMH:\n    ISO_639_1 = ''\n    ISO_639 = 'amh'\n    ENGLISH_NAME = 'Amharic'\n\n\nclass ANP:\n    ISO_639_1 = ''\n    ISO_639 = 'anp'\n    ENGLISH_NAME = 'Angika'\n\n\nclass ARA:\n    ISO_639_1 = ''\n    ISO_639 = 'ara'\n    ENGLISH_NAME = 'Arabic'\n\n\nclass ARG:\n    ISO_639_1 = ''\n    ISO_639 = 'arg'\n    ENGLISH_NAME = 'Aragonese'\n\n\nclass ARM:\n    ISO_639_1 = ''\n    ISO_639 = 'arm'\n    ENGLISH_NAME = 'Armenian'\n\n\nclass ARN:\n    ISO_639_1 = ''\n    ISO_639 = 'arn'\n    ENGLISH_NAME = 'Mapudungun'\n\n\nclass ARP:\n    ISO_639_1 = ''\n    ISO_639 = 'arp'\n    ENGLISH_NAME = 'Arapaho'\n\n\nclass ARW:\n    ISO_639_1 = ''\n    ISO_639 = 'arw'\n    ENGLISH_NAME = 'Arawak'\n\n\nclass ASM:\n    ISO_639_1 = ''\n    ISO_639 = 'asm'\n    ENGLISH_NAME = 'Assamese'\n\n\nclass AST:\n    ISO_639_1 = ''\n    ISO_639 = 'ast'\n    ENGLISH_NAME = 'Asturian'\n\n\nclass AVA:\n    ISO_639_1 = ''\n    ISO_639 = 'ava'\n    ENGLISH_NAME = 'Avaric'\n\n\nclass AVE:\n    ISO_639_1 = ''\n    ISO_639 = 'ave'\n    ENGLISH_NAME = 'Avestan'\n\n\nclass AWA:\n    ISO_639_1 = ''\n    ISO_639 = 'awa'\n    ENGLISH_NAME = 'Awadhi'\n\n\nclass AYM:\n    ISO_639_1 = ''\n    ISO_639 = 'aym'\n    ENGLISH_NAME = 'Aymara'\n\n\nclass AZE:\n    ISO_639_1 = ''\n    ISO_639 = 'aze'\n    ENGLISH_NAME = 'Azerbaijani'\n\n\nclass BAK:\n    ISO_639_1 = ''\n    ISO_639 = 'bak'\n    ENGLISH_NAME = 'Bashkir'\n\n\nclass BAL:\n    ISO_639_1 = ''\n    ISO_639 = 'bal'\n    ENGLISH_NAME = 'Baluchi'\n\n\nclass BAM:\n    ISO_639_1 = ''\n    ISO_639 = 'bam'\n    ENGLISH_NAME = 'Bambara'\n\n\nclass BAN:\n    ISO_639_1 = ''\n    ISO_639 = 'ban'\n    ENGLISH_NAME = 'Balinese'\n\n\nclass BAQ:\n    ISO_639_1 = ''\n    ISO_639 = 'baq'\n    ENGLISH_NAME = 'Basque'\n\n\nclass BAS:\n    ISO_639_1 = ''\n    ISO_639 = 'bas'\n    ENGLISH_NAME = 'Basa'\n\n\nclass BEJ:\n    ISO_639_1 = ''\n    ISO_639 = 'bej'\n    ENGLISH_NAME = 'Beja'\n\n\nclass BEL:\n    ISO_639_1 = ''\n    ISO_639 = 'bel'\n    ENGLISH_NAME = 'Belarusian'\n\n\nclass BEM:\n    ISO_639_1 = ''\n    ISO_639 = 'bem'\n    ENGLISH_NAME = 'Bemba'\n\n\nclass BEN:\n    ISO_639_1 = 'bn'\n    ISO_639 = 'ben'\n    ENGLISH_NAME = 'Bengali'\n\n\nclass BHO:\n    ISO_639_1 = ''\n    ISO_639 = 'bho'\n    ENGLISH_NAME = 'Bhojpuri'\n\n\nclass BIK:\n    ISO_639_1 = ''\n    ISO_639 = 'bik'\n    ENGLISH_NAME = 'Bikol'\n\n\nclass BIN:\n    ISO_639_1 = ''\n    ISO_639 = 'bin'\n    ENGLISH_NAME = 'Bini'\n\n\nclass BIS:\n    ISO_639_1 = ''\n    ISO_639 = 'bis'\n    ENGLISH_NAME = 'Bislama'\n\n\nclass BLA:\n    ISO_639_1 = ''\n    ISO_639 = 'bla'\n    ENGLISH_NAME = 'Siksika'\n\n\nclass BOS:\n    ISO_639_1 = ''\n    ISO_639 = 'bos'\n    ENGLISH_NAME = 'Bosnian'\n\n\nclass BRA:\n    ISO_639_1 = ''\n    ISO_639 = 'bra'\n    ENGLISH_NAME = 'Braj'\n\n\nclass BRE:\n    ISO_639_1 = ''\n    ISO_639 = 'bre'\n    ENGLISH_NAME = 'Breton'\n\n\nclass BUA:\n    ISO_639_1 = ''\n    ISO_639 = 'bua'\n    ENGLISH_NAME = 'Buriat'\n\n\nclass BUG:\n    ISO_639_1 = ''\n    ISO_639 = 'bug'\n    ENGLISH_NAME = 'Buginese'\n\n\nclass BUL:\n    ISO_639_1 = ''\n    ISO_639 = 'bul'\n    ENGLISH_NAME = 'Bulgarian'\n\n\nclass BUR:\n    ISO_639_1 = ''\n    ISO_639 = 'bur'\n    ENGLISH_NAME = 'Burmese'\n\n\nclass BYN:\n    ISO_639_1 = ''\n    ISO_639 = 'byn'\n    ENGLISH_NAME = 'Blin'\n\n\nclass CAD:\n    ISO_639_1 = ''\n    ISO_639 = 'cad'\n    ENGLISH_NAME = 'Caddo'\n\n\nclass CAR:\n    ISO_639_1 = ''\n    ISO_639 = 'car'\n    ENGLISH_NAME = 'GalibiCarib'\n\n\nclass CAT:\n    ISO_639_1 = ''\n    ISO_639 = 'cat'\n    ENGLISH_NAME = 'Catalan'\n\n\nclass CEB:\n    ISO_639_1 = ''\n    ISO_639 = 'ceb'\n    ENGLISH_NAME = 'Cebuano'\n\n\nclass CHA:\n    ISO_639_1 = ''\n    ISO_639 = 'cha'\n    ENGLISH_NAME = 'Chamorro'\n\n\nclass CHB:\n    ISO_639_1 = ''\n    ISO_639 = 'chb'\n    ENGLISH_NAME = 'Chibcha'\n\n\nclass CHE:\n    ISO_639_1 = ''\n    ISO_639 = 'che'\n    ENGLISH_NAME = 'Chechen'\n\n\nclass CHG:\n    ISO_639_1 = ''\n    ISO_639 = 'chg'\n    ENGLISH_NAME = 'Chagatai'\n\n\nclass CHI:\n    ISO_639_1 = 'zh'\n    ISO_639 = 'chi'\n    ENGLISH_NAME = 'Chinese'\n\n\nclass CHK:\n    ISO_639_1 = ''\n    ISO_639 = 'chk'\n    ENGLISH_NAME = 'Chuukese'\n\n\nclass CHM:\n    ISO_639_1 = ''\n    ISO_639 = 'chm'\n    ENGLISH_NAME = 'Mari'\n\n\nclass CHN:\n    ISO_639_1 = ''\n    ISO_639 = 'chn'\n    ENGLISH_NAME = 'Chinookjargon'\n\n\nclass CHO:\n    ISO_639_1 = ''\n    ISO_639 = 'cho'\n    ENGLISH_NAME = 'Choctaw'\n\n\nclass CHP:\n    ISO_639_1 = ''\n    ISO_639 = 'chp'\n    ENGLISH_NAME = 'Chipewyan'\n\n\nclass CHR:\n    ISO_639_1 = ''\n    ISO_639 = 'chr'\n    ENGLISH_NAME = 'Cherokee'\n\n\nclass CHV:\n    ISO_639_1 = ''\n    ISO_639 = 'chv'\n    ENGLISH_NAME = 'Chuvash'\n\n\nclass CHY:\n    ISO_639_1 = ''\n    ISO_639 = 'chy'\n    ENGLISH_NAME = 'Cheyenne'\n\n\nclass CNR:\n    ISO_639_1 = ''\n    ISO_639 = 'cnr'\n    ENGLISH_NAME = 'Montenegrin'\n\n\nclass COP:\n    ISO_639_1 = ''\n    ISO_639 = 'cop'\n    ENGLISH_NAME = 'Coptic'\n\n\nclass COR:\n    ISO_639_1 = ''\n    ISO_639 = 'cor'\n    ENGLISH_NAME = 'Cornish'\n\n\nclass COS:\n    ISO_639_1 = ''\n    ISO_639 = 'cos'\n    ENGLISH_NAME = 'Corsican'\n\n\nclass CPE:\n    ISO_639_1 = ''\n    ISO_639 = 'cpe'\n    ENGLISH_NAME = 'Creolesandpidgins'\n\n\nclass CPF:\n    ISO_639_1 = ''\n    ISO_639 = 'cpf'\n    ENGLISH_NAME = 'Creolesandpidgins'\n\n\nclass CPP:\n    ISO_639_1 = ''\n    ISO_639 = 'cpp'\n    ENGLISH_NAME = 'Creolesandpidgins'\n\n\nclass CRE:\n    ISO_639_1 = ''\n    ISO_639 = 'cre'\n    ENGLISH_NAME = 'Cree'\n\n\nclass CRH:\n    ISO_639_1 = ''\n    ISO_639 = 'crh'\n    ENGLISH_NAME = 'CrimeanTatar'\n\n\nclass CRP:\n    ISO_639_1 = ''\n    ISO_639 = 'crp'\n    ENGLISH_NAME = 'Creolesandpidgins'\n\n\nclass CSB:\n    ISO_639_1 = ''\n    ISO_639 = 'csb'\n    ENGLISH_NAME = 'Kashubian'\n\n\nclass CZE:\n    ISO_639_1 = ''\n    ISO_639 = 'cze'\n    ENGLISH_NAME = 'Czech'\n\n\nclass DAK:\n    ISO_639_1 = ''\n    ISO_639 = 'dak'\n    ENGLISH_NAME = 'Dakota'\n\n\nclass DAN:\n    ISO_639_1 = ''\n    ISO_639 = 'dan'\n    ENGLISH_NAME = 'Danish'\n\n\nclass DAR:\n    ISO_639_1 = ''\n    ISO_639 = 'dar'\n    ENGLISH_NAME = 'Dargwa'\n\n\nclass DEL:\n    ISO_639_1 = ''\n    ISO_639 = 'del'\n    ENGLISH_NAME = 'Delaware'\n\n\nclass DEN:\n    ISO_639_1 = ''\n    ISO_639 = 'den'\n    ENGLISH_NAME = 'Slave'\n\n\nclass DGR:\n    ISO_639_1 = ''\n    ISO_639 = 'dgr'\n    ENGLISH_NAME = 'Dogrib'\n\n\nclass DIN:\n    ISO_639_1 = ''\n    ISO_639 = 'din'\n    ENGLISH_NAME = 'Dinka'\n\n\nclass DIV:\n    ISO_639_1 = ''\n    ISO_639 = 'div'\n    ENGLISH_NAME = 'Divehi'\n\n\nclass DOI:\n    ISO_639_1 = ''\n    ISO_639 = 'doi'\n    ENGLISH_NAME = 'Dogri'\n\n\nclass DUA:\n    ISO_639_1 = ''\n    ISO_639 = 'dua'\n    ENGLISH_NAME = 'Duala'\n\n\nclass DUT:\n    ISO_639_1 = 'nl'\n    ISO_639 = 'dut'\n    ENGLISH_NAME = 'Dutch'\n\n\nclass DYU:\n    ISO_639_1 = ''\n    ISO_639 = 'dyu'\n    ENGLISH_NAME = 'Dyula'\n\n\nclass DZO:\n    ISO_639_1 = ''\n    ISO_639 = 'dzo'\n    ENGLISH_NAME = 'Dzongkha'\n\n\nclass EFI:\n    ISO_639_1 = ''\n    ISO_639 = 'efi'\n    ENGLISH_NAME = 'Efik'\n\n\nclass EKA:\n    ISO_639_1 = ''\n    ISO_639 = 'eka'\n    ENGLISH_NAME = 'Ekajuk'\n\n\nclass ELX:\n    ISO_639_1 = ''\n    ISO_639 = 'elx'\n    ENGLISH_NAME = 'Elamite'\n\n\nclass ENG:\n    ISO_639_1 = 'en'\n    ISO_639 = 'eng'\n    ENGLISH_NAME = 'English'\n\n\nclass EPO:\n    ISO_639_1 = ''\n    ISO_639 = 'epo'\n    ENGLISH_NAME = 'Esperanto'\n\n\nclass EST:\n    ISO_639_1 = ''\n    ISO_639 = 'est'\n    ENGLISH_NAME = 'Estonian'\n\n\nclass EWE:\n    ISO_639_1 = ''\n    ISO_639 = 'ewe'\n    ENGLISH_NAME = 'Ewe'\n\n\nclass EWO:\n    ISO_639_1 = ''\n    ISO_639 = 'ewo'\n    ENGLISH_NAME = 'Ewondo'\n\n\nclass FAN:\n    ISO_639_1 = ''\n    ISO_639 = 'fan'\n    ENGLISH_NAME = 'Fang'\n\n\nclass FAO:\n    ISO_639_1 = ''\n    ISO_639 = 'fao'\n    ENGLISH_NAME = 'Faroese'\n\n\nclass FAT:\n    ISO_639_1 = ''\n    ISO_639 = 'fat'\n    ENGLISH_NAME = 'Fanti'\n\n\nclass FIJ:\n    ISO_639_1 = ''\n    ISO_639 = 'fij'\n    ENGLISH_NAME = 'Fijian'\n\n\nclass FIL:\n    ISO_639_1 = ''\n    ISO_639 = 'fil'\n    ENGLISH_NAME = 'Filipino'\n\n\nclass FIN:\n    ISO_639_1 = ''\n    ISO_639 = 'fin'\n    ENGLISH_NAME = 'Finnish'\n\n\nclass FON:\n    ISO_639_1 = ''\n    ISO_639 = 'fon'\n    ENGLISH_NAME = 'Fon'\n\n\nclass FRE:\n    ISO_639_1 = ''\n    ISO_639 = 'fre'\n    ENGLISH_NAME = 'French'\n\n\nclass FRR:\n    ISO_639_1 = ''\n    ISO_639 = 'frr'\n    ENGLISH_NAME = 'NorthernFrisian'\n\n\nclass FRS:\n    ISO_639_1 = ''\n    ISO_639 = 'frs'\n    ENGLISH_NAME = 'EasternFrisian'\n\n\nclass FRY:\n    ISO_639_1 = ''\n    ISO_639 = 'fry'\n    ENGLISH_NAME = 'WesternFrisian'\n\n\nclass FUL:\n    ISO_639_1 = ''\n    ISO_639 = 'ful'\n    ENGLISH_NAME = 'Fulah'\n\n\nclass FUR:\n    ISO_639_1 = ''\n    ISO_639 = 'fur'\n    ENGLISH_NAME = 'Friulian'\n\n\nclass GAA:\n    ISO_639_1 = ''\n    ISO_639 = 'gaa'\n    ENGLISH_NAME = 'Ga'\n\n\nclass GAY:\n    ISO_639_1 = ''\n    ISO_639 = 'gay'\n    ENGLISH_NAME = 'Gayo'\n\n\nclass GBA:\n    ISO_639_1 = ''\n    ISO_639 = 'gba'\n    ENGLISH_NAME = 'Gbaya'\n\n\nclass GEO:\n    ISO_639_1 = ''\n    ISO_639 = 'geo'\n    ENGLISH_NAME = 'Georgian'\n\n\nclass GER:\n    ISO_639_1 = 'de'\n    ISO_639 = 'ger'\n    ENGLISH_NAME = 'German'\n\n\nclass GEZ:\n    ISO_639_1 = ''\n    ISO_639 = 'gez'\n    ENGLISH_NAME = 'Geez'\n\n\nclass GIL:\n    ISO_639_1 = ''\n    ISO_639 = 'gil'\n    ENGLISH_NAME = 'Gilbertese'\n\n\nclass GLA:\n    ISO_639_1 = ''\n    ISO_639 = 'gla'\n    ENGLISH_NAME = 'Gaelic'\n\n\nclass GLE:\n    ISO_639_1 = ''\n    ISO_639 = 'gle'\n    ENGLISH_NAME = 'Irish'\n\n\nclass GLG:\n    ISO_639_1 = ''\n    ISO_639 = 'glg'\n    ENGLISH_NAME = 'Galician'\n\n\nclass GLV:\n    ISO_639_1 = ''\n    ISO_639 = 'glv'\n    ENGLISH_NAME = 'Manx'\n\n\nclass GON:\n    ISO_639_1 = ''\n    ISO_639 = 'gon'\n    ENGLISH_NAME = 'Gondi'\n\n\nclass GOR:\n    ISO_639_1 = ''\n    ISO_639 = 'gor'\n    ENGLISH_NAME = 'Gorontalo'\n\n\nclass GOT:\n    ISO_639_1 = ''\n    ISO_639 = 'got'\n    ENGLISH_NAME = 'Gothic'\n\n\nclass GRB:\n    ISO_639_1 = ''\n    ISO_639 = 'grb'\n    ENGLISH_NAME = 'Grebo'\n\n\nclass GRE:\n    ISO_639_1 = 'el'\n    ISO_639 = 'gre'\n    ENGLISH_NAME = 'Greek'\n\n\nclass GRN:\n    ISO_639_1 = ''\n    ISO_639 = 'grn'\n    ENGLISH_NAME = 'Guarani'\n\n\nclass GSW:\n    ISO_639_1 = ''\n    ISO_639 = 'gsw'\n    ENGLISH_NAME = 'SwissGerman'\n\n\nclass GUJ:\n    ISO_639_1 = ''\n    ISO_639 = 'guj'\n    ENGLISH_NAME = 'Gujarati'\n\n\nclass GWI:\n    ISO_639_1 = ''\n    ISO_639 = 'gwi'\n    ENGLISH_NAME = 'Gwichin'\n\n\nclass HAI:\n    ISO_639_1 = ''\n    ISO_639 = 'hai'\n    ENGLISH_NAME = 'Haida'\n\n\nclass HAT:\n    ISO_639_1 = ''\n    ISO_639 = 'hat'\n    ENGLISH_NAME = 'Haitian'\n\n\nclass HAU:\n    ISO_639_1 = ''\n    ISO_639 = 'hau'\n    ENGLISH_NAME = 'Hausa'\n\n\nclass HAW:\n    ISO_639_1 = ''\n    ISO_639 = 'haw'\n    ENGLISH_NAME = 'Hawaiian'\n\n\nclass HEB:\n    ISO_639_1 = 'he'\n    ISO_639 = 'heb'\n    ENGLISH_NAME = 'Hebrew'\n\n\nclass HER:\n    ISO_639_1 = ''\n    ISO_639 = 'her'\n    ENGLISH_NAME = 'Herero'\n\n\nclass HIL:\n    ISO_639_1 = ''\n    ISO_639 = 'hil'\n    ENGLISH_NAME = 'Hiligaynon'\n\n\nclass HIN:\n    ISO_639_1 = 'hi'\n    ISO_639 = 'hin'\n    ENGLISH_NAME = 'Hindi'\n\n\nclass HIT:\n    ISO_639_1 = ''\n    ISO_639 = 'hit'\n    ENGLISH_NAME = 'Hittite'\n\n\nclass HMN:\n    ISO_639_1 = ''\n    ISO_639 = 'hmn'\n    ENGLISH_NAME = 'Hmong'\n\n\nclass HMO:\n    ISO_639_1 = ''\n    ISO_639 = 'hmo'\n    ENGLISH_NAME = 'HiriMotu'\n\n\nclass HRV:\n    ISO_639_1 = ''\n    ISO_639 = 'hrv'\n    ENGLISH_NAME = 'Croatian'\n\n\nclass HSB:\n    ISO_639_1 = ''\n    ISO_639 = 'hsb'\n    ENGLISH_NAME = 'UpperSorbian'\n\n\nclass HUN:\n    ISO_639_1 = ''\n    ISO_639 = 'hun'\n    ENGLISH_NAME = 'Hungarian'\n\n\nclass HUP:\n    ISO_639_1 = ''\n    ISO_639 = 'hup'\n    ENGLISH_NAME = 'Hupa'\n\n\nclass IBA:\n    ISO_639_1 = ''\n    ISO_639 = 'iba'\n    ENGLISH_NAME = 'Iban'\n\n\nclass IBO:\n    ISO_639_1 = ''\n    ISO_639 = 'ibo'\n    ENGLISH_NAME = 'Igbo'\n\n\nclass ICE:\n    ISO_639_1 = ''\n    ISO_639 = 'ice'\n    ENGLISH_NAME = 'Icelandic'\n\n\nclass IDO:\n    ISO_639_1 = ''\n    ISO_639 = 'ido'\n    ENGLISH_NAME = 'Ido'\n\n\nclass III:\n    ISO_639_1 = ''\n    ISO_639 = 'iii'\n    ENGLISH_NAME = 'SichuanYi'\n\n\nclass IKU:\n    ISO_639_1 = ''\n    ISO_639 = 'iku'\n    ENGLISH_NAME = 'Inuktitut'\n\n\nclass ILE:\n    ISO_639_1 = ''\n    ISO_639 = 'ile'\n    ENGLISH_NAME = 'Interlingue'\n\n\nclass ILO:\n    ISO_639_1 = ''\n    ISO_639 = 'ilo'\n    ENGLISH_NAME = 'Iloko'\n\n\nclass INA:\n    ISO_639_1 = ''\n    ISO_639 = 'ina'\n    ENGLISH_NAME = 'Interlingua'\n\n\nclass IND:\n    ISO_639_1 = 'id'\n    ISO_639 = 'ind'\n    ENGLISH_NAME = 'Indonesian'\n\n\nclass INH:\n    ISO_639_1 = ''\n    ISO_639 = 'inh'\n    ENGLISH_NAME = 'Ingush'\n\n\nclass IPK:\n    ISO_639_1 = ''\n    ISO_639 = 'ipk'\n    ENGLISH_NAME = 'Inupiaq'\n\n\nclass ITA:\n    ISO_639_1 = ''\n    ISO_639 = 'ita'\n    ENGLISH_NAME = 'Italian'\n\n\nclass JAV:\n    ISO_639_1 = ''\n    ISO_639 = 'jav'\n    ENGLISH_NAME = 'Javanese'\n\n\nclass JBO:\n    ISO_639_1 = ''\n    ISO_639 = 'jbo'\n    ENGLISH_NAME = 'Lojban'\n\n\nclass JPN:\n    ISO_639_1 = 'ja'\n    ISO_639 = 'jpn'\n    ENGLISH_NAME = 'Japanese'\n\n\nclass JPR:\n    ISO_639_1 = ''\n    ISO_639 = 'jpr'\n    ENGLISH_NAME = 'JudeoPersian'\n\n\nclass JRB:\n    ISO_639_1 = ''\n    ISO_639 = 'jrb'\n    ENGLISH_NAME = 'JudeoArabic'\n\n\nclass KAA:\n    ISO_639_1 = ''\n    ISO_639 = 'kaa'\n    ENGLISH_NAME = 'KaraKalpak'\n\n\nclass KAB:\n    ISO_639_1 = ''\n    ISO_639 = 'kab'\n    ENGLISH_NAME = 'Kabyle'\n\n\nclass KAC:\n    ISO_639_1 = ''\n    ISO_639 = 'kac'\n    ENGLISH_NAME = 'Kachin'\n\n\nclass KAL:\n    ISO_639_1 = ''\n    ISO_639 = 'kal'\n    ENGLISH_NAME = 'Kalaallisut'\n\n\nclass KAM:\n    ISO_639_1 = ''\n    ISO_639 = 'kam'\n    ENGLISH_NAME = 'Kamba'\n\n\nclass KAN:\n    ISO_639_1 = ''\n    ISO_639 = 'kan'\n    ENGLISH_NAME = 'Kannada'\n\n\nclass KAS:\n    ISO_639_1 = ''\n    ISO_639 = 'kas'\n    ENGLISH_NAME = 'Kashmiri'\n\n\nclass KAU:\n    ISO_639_1 = ''\n    ISO_639 = 'kau'\n    ENGLISH_NAME = 'Kanuri'\n\n\nclass KAW:\n    ISO_639_1 = ''\n    ISO_639 = 'kaw'\n    ENGLISH_NAME = 'Kawi'\n\n\nclass KAZ:\n    ISO_639_1 = ''\n    ISO_639 = 'kaz'\n    ENGLISH_NAME = 'Kazakh'\n\n\nclass KBD:\n    ISO_639_1 = ''\n    ISO_639 = 'kbd'\n    ENGLISH_NAME = 'Kabardian'\n\n\nclass KHA:\n    ISO_639_1 = ''\n    ISO_639 = 'kha'\n    ENGLISH_NAME = 'Khasi'\n\n\nclass KHM:\n    ISO_639_1 = ''\n    ISO_639 = 'khm'\n    ENGLISH_NAME = 'CentralKhmer'\n\n\nclass KHO:\n    ISO_639_1 = ''\n    ISO_639 = 'kho'\n    ENGLISH_NAME = 'Khotanese'\n\n\nclass KIK:\n    ISO_639_1 = ''\n    ISO_639 = 'kik'\n    ENGLISH_NAME = 'Kikuyu'\n\n\nclass KIN:\n    ISO_639_1 = ''\n    ISO_639 = 'kin'\n    ENGLISH_NAME = 'Kinyarwanda'\n\n\nclass KIR:\n    ISO_639_1 = ''\n    ISO_639 = 'kir'\n    ENGLISH_NAME = 'Kirghiz'\n\n\nclass KMB:\n    ISO_639_1 = ''\n    ISO_639 = 'kmb'\n    ENGLISH_NAME = 'Kimbundu'\n\n\nclass KOK:\n    ISO_639_1 = ''\n    ISO_639 = 'kok'\n    ENGLISH_NAME = 'Konkani'\n\n\nclass KOM:\n    ISO_639_1 = ''\n    ISO_639 = 'kom'\n    ENGLISH_NAME = 'Komi'\n\n\nclass KON:\n    ISO_639_1 = ''\n    ISO_639 = 'kon'\n    ENGLISH_NAME = 'Kongo'\n\n\nclass KOR:\n    ISO_639_1 = 'ko'\n    ISO_639 = 'kor'\n    ENGLISH_NAME = 'Korean'\n\n\nclass KOS:\n    ISO_639_1 = ''\n    ISO_639 = 'kos'\n    ENGLISH_NAME = 'Kosraean'\n\n\nclass KPE:\n    ISO_639_1 = ''\n    ISO_639 = 'kpe'\n    ENGLISH_NAME = 'Kpelle'\n\n\nclass KRC:\n    ISO_639_1 = ''\n    ISO_639 = 'krc'\n    ENGLISH_NAME = 'KarachayBalkar'\n\n\nclass KRL:\n    ISO_639_1 = ''\n    ISO_639 = 'krl'\n    ENGLISH_NAME = 'Karelian'\n\n\nclass KRU:\n    ISO_639_1 = ''\n    ISO_639 = 'kru'\n    ENGLISH_NAME = 'Kurukh'\n\n\nclass KUA:\n    ISO_639_1 = ''\n    ISO_639 = 'kua'\n    ENGLISH_NAME = 'Kuanyama'\n\n\nclass KUM:\n    ISO_639_1 = ''\n    ISO_639 = 'kum'\n    ENGLISH_NAME = 'Kumyk'\n\n\nclass KUR:\n    ISO_639_1 = ''\n    ISO_639 = 'kur'\n    ENGLISH_NAME = 'Kurdish'\n\n\nclass KUT:\n    ISO_639_1 = ''\n    ISO_639 = 'kut'\n    ENGLISH_NAME = 'Kutenai'\n\n\nclass LAD:\n    ISO_639_1 = ''\n    ISO_639 = 'lad'\n    ENGLISH_NAME = 'Ladino'\n\n\nclass LAH:\n    ISO_639_1 = ''\n    ISO_639 = 'lah'\n    ENGLISH_NAME = 'Lahnda'\n\n\nclass LAM:\n    ISO_639_1 = ''\n    ISO_639 = 'lam'\n    ENGLISH_NAME = 'Lamba'\n\n\nclass LAO:\n    ISO_639_1 = ''\n    ISO_639 = 'lao'\n    ENGLISH_NAME = 'Lao'\n\n\nclass LAT:\n    ISO_639_1 = ''\n    ISO_639 = 'lat'\n    ENGLISH_NAME = 'Latin'\n\n\nclass LAV:\n    ISO_639_1 = ''\n    ISO_639 = 'lav'\n    ENGLISH_NAME = 'Latvian'\n\n\nclass LEZ:\n    ISO_639_1 = ''\n    ISO_639 = 'lez'\n    ENGLISH_NAME = 'Lezghian'\n\n\nclass LIM:\n    ISO_639_1 = ''\n    ISO_639 = 'lim'\n    ENGLISH_NAME = 'Limburgan'\n\n\nclass LIN:\n    ISO_639_1 = ''\n    ISO_639 = 'lin'\n    ENGLISH_NAME = 'Lingala'\n\n\nclass LIT:\n    ISO_639_1 = ''\n    ISO_639 = 'lit'\n    ENGLISH_NAME = 'Lithuanian'\n\n\nclass LOL:\n    ISO_639_1 = ''\n    ISO_639 = 'lol'\n    ENGLISH_NAME = 'Mongo'\n\n\nclass LOZ:\n    ISO_639_1 = ''\n    ISO_639 = 'loz'\n    ENGLISH_NAME = 'Lozi'\n\n\nclass LTZ:\n    ISO_639_1 = ''\n    ISO_639 = 'ltz'\n    ENGLISH_NAME = 'Luxembourgish'\n\n\nclass LUA:\n    ISO_639_1 = ''\n    ISO_639 = 'lua'\n    ENGLISH_NAME = 'LubaLulua'\n\n\nclass LUB:\n    ISO_639_1 = ''\n    ISO_639 = 'lub'\n    ENGLISH_NAME = 'LubaKatanga'\n\n\nclass LUG:\n    ISO_639_1 = ''\n    ISO_639 = 'lug'\n    ENGLISH_NAME = 'Ganda'\n\n\nclass LUI:\n    ISO_639_1 = ''\n    ISO_639 = 'lui'\n    ENGLISH_NAME = 'Luiseno'\n\n\nclass LUN:\n    ISO_639_1 = ''\n    ISO_639 = 'lun'\n    ENGLISH_NAME = 'Lunda'\n\n\nclass LUO:\n    ISO_639_1 = ''\n    ISO_639 = 'luo'\n    ENGLISH_NAME = 'Luo'\n\n\nclass LUS:\n    ISO_639_1 = ''\n    ISO_639 = 'lus'\n    ENGLISH_NAME = 'Lushai'\n\n\nclass MAC:\n    ISO_639_1 = ''\n    ISO_639 = 'mac'\n    ENGLISH_NAME = 'Macedonian'\n\n\nclass MAD:\n    ISO_639_1 = ''\n    ISO_639 = 'mad'\n    ENGLISH_NAME = 'Madurese'\n\n\nclass MAG:\n    ISO_639_1 = ''\n    ISO_639 = 'mag'\n    ENGLISH_NAME = 'Magahi'\n\n\nclass MAH:\n    ISO_639_1 = ''\n    ISO_639 = 'mah'\n    ENGLISH_NAME = 'Marshallese'\n\n\nclass MAI:\n    ISO_639_1 = ''\n    ISO_639 = 'mai'\n    ENGLISH_NAME = 'Maithili'\n\n\nclass MAK:\n    ISO_639_1 = ''\n    ISO_639 = 'mak'\n    ENGLISH_NAME = 'Makasar'\n\n\nclass MAL:\n    ISO_639_1 = ''\n    ISO_639 = 'mal'\n    ENGLISH_NAME = 'Malayalam'\n\n\nclass MAN:\n    ISO_639_1 = ''\n    ISO_639 = 'man'\n    ENGLISH_NAME = 'Mandingo'\n\n\nclass MAO:\n    ISO_639_1 = ''\n    ISO_639 = 'mao'\n    ENGLISH_NAME = 'Maori'\n\n\nclass MAR:\n    ISO_639_1 = 'mr'\n    ISO_639 = 'mar'\n    ENGLISH_NAME = 'Marathi'\n\n\nclass MAS:\n    ISO_639_1 = ''\n    ISO_639 = 'mas'\n    ENGLISH_NAME = 'Masai'\n\n\nclass MAY:\n    ISO_639_1 = ''\n    ISO_639 = 'may'\n    ENGLISH_NAME = 'Malay'\n\n\nclass MDF:\n    ISO_639_1 = ''\n    ISO_639 = 'mdf'\n    ENGLISH_NAME = 'Moksha'\n\n\nclass MDR:\n    ISO_639_1 = ''\n    ISO_639 = 'mdr'\n    ENGLISH_NAME = 'Mandar'\n\n\nclass MEN:\n    ISO_639_1 = ''\n    ISO_639 = 'men'\n    ENGLISH_NAME = 'Mende'\n\n\nclass MIC:\n    ISO_639_1 = ''\n    ISO_639 = 'mic'\n    ENGLISH_NAME = 'Mikmaq'\n\n\nclass MIN:\n    ISO_639_1 = ''\n    ISO_639 = 'min'\n    ENGLISH_NAME = 'Minangkabau'\n\n\nclass MLG:\n    ISO_639_1 = ''\n    ISO_639 = 'mlg'\n    ENGLISH_NAME = 'Malagasy'\n\n\nclass MLT:\n    ISO_639_1 = ''\n    ISO_639 = 'mlt'\n    ENGLISH_NAME = 'Maltese'\n\n\nclass MNC:\n    ISO_639_1 = ''\n    ISO_639 = 'mnc'\n    ENGLISH_NAME = 'Manchu'\n\n\nclass MNI:\n    ISO_639_1 = ''\n    ISO_639 = 'mni'\n    ENGLISH_NAME = 'Manipuri'\n\n\nclass MOH:\n    ISO_639_1 = ''\n    ISO_639 = 'moh'\n    ENGLISH_NAME = 'Mohawk'\n\n\nclass MON:\n    ISO_639_1 = ''\n    ISO_639 = 'mon'\n    ENGLISH_NAME = 'Mongolian'\n\n\nclass MOS:\n    ISO_639_1 = ''\n    ISO_639 = 'mos'\n    ENGLISH_NAME = 'Mossi'\n\n\nclass MUS:\n    ISO_639_1 = ''\n    ISO_639 = 'mus'\n    ENGLISH_NAME = 'Creek'\n\n\nclass MWL:\n    ISO_639_1 = ''\n    ISO_639 = 'mwl'\n    ENGLISH_NAME = 'Mirandese'\n\n\nclass MWR:\n    ISO_639_1 = ''\n    ISO_639 = 'mwr'\n    ENGLISH_NAME = 'Marwari'\n\n\nclass MYV:\n    ISO_639_1 = ''\n    ISO_639 = 'myv'\n    ENGLISH_NAME = 'Erzya'\n\n\nclass NAP:\n    ISO_639_1 = ''\n    ISO_639 = 'nap'\n    ENGLISH_NAME = 'Neapolitan'\n\n\nclass NAU:\n    ISO_639_1 = ''\n    ISO_639 = 'nau'\n    ENGLISH_NAME = 'Nauru'\n\n\nclass NAV:\n    ISO_639_1 = ''\n    ISO_639 = 'nav'\n    ENGLISH_NAME = 'Navajo'\n\n\nclass NBL:\n    ISO_639_1 = ''\n    ISO_639 = 'nbl'\n    ENGLISH_NAME = 'Ndebele'\n\n\nclass NDE:\n    ISO_639_1 = ''\n    ISO_639 = 'nde'\n    ENGLISH_NAME = 'Ndebele'\n\n\nclass NDO:\n    ISO_639_1 = ''\n    ISO_639 = 'ndo'\n    ENGLISH_NAME = 'Ndonga'\n\n\nclass NEP:\n    ISO_639_1 = ''\n    ISO_639 = 'nep'\n    ENGLISH_NAME = 'Nepali'\n\n\nclass NEW:\n    ISO_639_1 = ''\n    ISO_639 = 'new'\n    ENGLISH_NAME = 'NepalBhasa'\n\n\nclass NIA:\n    ISO_639_1 = ''\n    ISO_639 = 'nia'\n    ENGLISH_NAME = 'Nias'\n\n\nclass NIU:\n    ISO_639_1 = ''\n    ISO_639 = 'niu'\n    ENGLISH_NAME = 'Niuean'\n\n\nclass NNO:\n    ISO_639_1 = ''\n    ISO_639 = 'nno'\n    ENGLISH_NAME = 'NorwegianNynorsk'\n\n\nclass NOB:\n    ISO_639_1 = ''\n    ISO_639 = 'nob'\n    ENGLISH_NAME = 'Bokmål'\n\n\nclass NOG:\n    ISO_639_1 = ''\n    ISO_639 = 'nog'\n    ENGLISH_NAME = 'Nogai'\n\n\nclass NOR:\n    ISO_639_1 = ''\n    ISO_639 = 'nor'\n    ENGLISH_NAME = 'Norwegian'\n\n\nclass NQO:\n    ISO_639_1 = ''\n    ISO_639 = 'nqo'\n    ENGLISH_NAME = 'NKo'\n\n\nclass NSO:\n    ISO_639_1 = ''\n    ISO_639 = 'nso'\n    ENGLISH_NAME = 'Pedi'\n\n\nclass NYA:\n    ISO_639_1 = ''\n    ISO_639 = 'nya'\n    ENGLISH_NAME = 'Chichewa'\n\n\nclass NYM:\n    ISO_639_1 = ''\n    ISO_639 = 'nym'\n    ENGLISH_NAME = 'Nyamwezi'\n\n\nclass NYN:\n    ISO_639_1 = ''\n    ISO_639 = 'nyn'\n    ENGLISH_NAME = 'Nyankole'\n\n\nclass NYO:\n    ISO_639_1 = ''\n    ISO_639 = 'nyo'\n    ENGLISH_NAME = 'Nyoro'\n\n\nclass NZI:\n    ISO_639_1 = ''\n    ISO_639 = 'nzi'\n    ENGLISH_NAME = 'Nzima'\n\n\nclass OJI:\n    ISO_639_1 = ''\n    ISO_639 = 'oji'\n    ENGLISH_NAME = 'Ojibwa'\n\n\nclass ORI:\n    ISO_639_1 = 'or'\n    ISO_639 = 'ori'\n    ENGLISH_NAME = 'Oriya'\n\n\nclass ORM:\n    ISO_639_1 = ''\n    ISO_639 = 'orm'\n    ENGLISH_NAME = 'Oromo'\n\n\nclass OSA:\n    ISO_639_1 = ''\n    ISO_639 = 'osa'\n    ENGLISH_NAME = 'Osage'\n\n\nclass OSS:\n    ISO_639_1 = ''\n    ISO_639 = 'oss'\n    ENGLISH_NAME = 'Ossetian'\n\n\nclass PAG:\n    ISO_639_1 = ''\n    ISO_639 = 'pag'\n    ENGLISH_NAME = 'Pangasinan'\n\n\nclass PAL:\n    ISO_639_1 = ''\n    ISO_639 = 'pal'\n    ENGLISH_NAME = 'Pahlavi'\n\n\nclass PAM:\n    ISO_639_1 = ''\n    ISO_639 = 'pam'\n    ENGLISH_NAME = 'Pampanga'\n\n\nclass PAN:\n    ISO_639_1 = ''\n    ISO_639 = 'pan'\n    ENGLISH_NAME = 'Panjabi'\n\n\nclass PAP:\n    ISO_639_1 = ''\n    ISO_639 = 'pap'\n    ENGLISH_NAME = 'Papiamento'\n\n\nclass PAU:\n    ISO_639_1 = ''\n    ISO_639 = 'pau'\n    ENGLISH_NAME = 'Palauan'\n\n\nclass PER:\n    ISO_639_1 = 'fa'\n    ISO_639 = 'per'\n    ENGLISH_NAME = 'Persian'\n\n\nclass PHN:\n    ISO_639_1 = ''\n    ISO_639 = 'phn'\n    ENGLISH_NAME = 'Phoenician'\n\n\nclass PLI:\n    ISO_639_1 = ''\n    ISO_639 = 'pli'\n    ENGLISH_NAME = 'Pali'\n\n\nclass POL:\n    ISO_639_1 = ''\n    ISO_639 = 'pol'\n    ENGLISH_NAME = 'Polish'\n\n\nclass PON:\n    ISO_639_1 = ''\n    ISO_639 = 'pon'\n    ENGLISH_NAME = 'Pohnpeian'\n\n\nclass POR:\n    ISO_639_1 = 'pt'\n    ISO_639 = 'por'\n    ENGLISH_NAME = 'Portuguese'\n\n\nclass PUS:\n    ISO_639_1 = ''\n    ISO_639 = 'pus'\n    ENGLISH_NAME = 'Pushto'\n\n\nclass QUE:\n    ISO_639_1 = ''\n    ISO_639 = 'que'\n    ENGLISH_NAME = 'Quechua'\n\n\nclass RAJ:\n    ISO_639_1 = ''\n    ISO_639 = 'raj'\n    ENGLISH_NAME = 'Rajasthani'\n\n\nclass RAP:\n    ISO_639_1 = ''\n    ISO_639 = 'rap'\n    ENGLISH_NAME = 'Rapanui'\n\n\nclass RAR:\n    ISO_639_1 = ''\n    ISO_639 = 'rar'\n    ENGLISH_NAME = 'Rarotongan'\n\n\nclass ROH:\n    ISO_639_1 = ''\n    ISO_639 = 'roh'\n    ENGLISH_NAME = 'Romansh'\n\n\nclass ROM:\n    ISO_639_1 = ''\n    ISO_639 = 'rom'\n    ENGLISH_NAME = 'Romany'\n\n\nclass RUM:\n    ISO_639_1 = ''\n    ISO_639 = 'rum'\n    ENGLISH_NAME = 'Romanian'\n\n\nclass RUN:\n    ISO_639_1 = ''\n    ISO_639 = 'run'\n    ENGLISH_NAME = 'Rundi'\n\n\nclass RUP:\n    ISO_639_1 = ''\n    ISO_639 = 'rup'\n    ENGLISH_NAME = 'Aromanian'\n\n\nclass RUS:\n    ISO_639_1 = 'ru'\n    ISO_639 = 'rus'\n    ENGLISH_NAME = 'Russian'\n\n\nclass SAD:\n    ISO_639_1 = ''\n    ISO_639 = 'sad'\n    ENGLISH_NAME = 'Sandawe'\n\n\nclass SAG:\n    ISO_639_1 = ''\n    ISO_639 = 'sag'\n    ENGLISH_NAME = 'Sango'\n\n\nclass SAH:\n    ISO_639_1 = ''\n    ISO_639 = 'sah'\n    ENGLISH_NAME = 'Yakut'\n\n\nclass SAM:\n    ISO_639_1 = ''\n    ISO_639 = 'sam'\n    ENGLISH_NAME = 'SamaritanAramaic'\n\n\nclass SAN:\n    ISO_639_1 = ''\n    ISO_639 = 'san'\n    ENGLISH_NAME = 'Sanskrit'\n\n\nclass SAS:\n    ISO_639_1 = ''\n    ISO_639 = 'sas'\n    ENGLISH_NAME = 'Sasak'\n\n\nclass SAT:\n    ISO_639_1 = ''\n    ISO_639 = 'sat'\n    ENGLISH_NAME = 'Santali'\n\n\nclass SCN:\n    ISO_639_1 = ''\n    ISO_639 = 'scn'\n    ENGLISH_NAME = 'Sicilian'\n\n\nclass SCO:\n    ISO_639_1 = ''\n    ISO_639 = 'sco'\n    ENGLISH_NAME = 'Scots'\n\n\nclass SEL:\n    ISO_639_1 = ''\n    ISO_639 = 'sel'\n    ENGLISH_NAME = 'Selkup'\n\n\nclass SHN:\n    ISO_639_1 = ''\n    ISO_639 = 'shn'\n    ENGLISH_NAME = 'Shan'\n\n\nclass SID:\n    ISO_639_1 = ''\n    ISO_639 = 'sid'\n    ENGLISH_NAME = 'Sidamo'\n\n\nclass SIN:\n    ISO_639_1 = ''\n    ISO_639 = 'sin'\n    ENGLISH_NAME = 'Sinhala'\n\n\nclass SLO:\n    ISO_639_1 = ''\n    ISO_639 = 'slo'\n    ENGLISH_NAME = 'Slovak'\n\n\nclass SLV:\n    ISO_639_1 = ''\n    ISO_639 = 'slv'\n    ENGLISH_NAME = 'Slovenian'\n\n\nclass SMA:\n    ISO_639_1 = ''\n    ISO_639 = 'sma'\n    ENGLISH_NAME = 'SouthernSami'\n\n\nclass SME:\n    ISO_639_1 = ''\n    ISO_639 = 'sme'\n    ENGLISH_NAME = 'NorthernSami'\n\n\nclass SMJ:\n    ISO_639_1 = ''\n    ISO_639 = 'smj'\n    ENGLISH_NAME = 'LuleSami'\n\n\nclass SMN:\n    ISO_639_1 = ''\n    ISO_639 = 'smn'\n    ENGLISH_NAME = 'InariSami'\n\n\nclass SMO:\n    ISO_639_1 = ''\n    ISO_639 = 'smo'\n    ENGLISH_NAME = 'Samoan'\n\n\nclass SMS:\n    ISO_639_1 = ''\n    ISO_639 = 'sms'\n    ENGLISH_NAME = 'SkoltSami'\n\n\nclass SNA:\n    ISO_639_1 = ''\n    ISO_639 = 'sna'\n    ENGLISH_NAME = 'Shona'\n\n\nclass SND:\n    ISO_639_1 = ''\n    ISO_639 = 'snd'\n    ENGLISH_NAME = 'Sindhi'\n\n\nclass SNK:\n    ISO_639_1 = ''\n    ISO_639 = 'snk'\n    ENGLISH_NAME = 'Soninke'\n\n\nclass SOG:\n    ISO_639_1 = ''\n    ISO_639 = 'sog'\n    ENGLISH_NAME = 'Sogdian'\n\n\nclass SOM:\n    ISO_639_1 = ''\n    ISO_639 = 'som'\n    ENGLISH_NAME = 'Somali'\n\n\nclass SOT:\n    ISO_639_1 = ''\n    ISO_639 = 'sot'\n    ENGLISH_NAME = 'Sotho'\n\n\nclass SPA:\n    ISO_639_1 = 'es'\n    ISO_639 = 'spa'\n    ENGLISH_NAME = 'Spanish'\n\n\nclass SRD:\n    ISO_639_1 = ''\n    ISO_639 = 'srd'\n    ENGLISH_NAME = 'Sardinian'\n\n\nclass SRN:\n    ISO_639_1 = ''\n    ISO_639 = 'srn'\n    ENGLISH_NAME = 'SrananTongo'\n\n\nclass SRP:\n    ISO_639_1 = ''\n    ISO_639 = 'srp'\n    ENGLISH_NAME = 'Serbian'\n\n\nclass SRR:\n    ISO_639_1 = ''\n    ISO_639 = 'srr'\n    ENGLISH_NAME = 'Serer'\n\n\nclass SSW:\n    ISO_639_1 = ''\n    ISO_639 = 'ssw'\n    ENGLISH_NAME = 'Swati'\n\n\nclass SUK:\n    ISO_639_1 = ''\n    ISO_639 = 'suk'\n    ENGLISH_NAME = 'Sukuma'\n\n\nclass SUN:\n    ISO_639_1 = ''\n    ISO_639 = 'sun'\n    ENGLISH_NAME = 'Sundanese'\n\n\nclass SUS:\n    ISO_639_1 = ''\n    ISO_639 = 'sus'\n    ENGLISH_NAME = 'Susu'\n\n\nclass SUX:\n    ISO_639_1 = ''\n    ISO_639 = 'sux'\n    ENGLISH_NAME = 'Sumerian'\n\n\nclass SWA:\n    ISO_639_1 = ''\n    ISO_639 = 'swa'\n    ENGLISH_NAME = 'Swahili'\n\n\nclass SWE:\n    ISO_639_1 = 'sv'\n    ISO_639 = 'swe'\n    ENGLISH_NAME = 'Swedish'\n\n\nclass SYC:\n    ISO_639_1 = ''\n    ISO_639 = 'syc'\n    ENGLISH_NAME = 'ClassicalSyriac'\n\n\nclass SYR:\n    ISO_639_1 = ''\n    ISO_639 = 'syr'\n    ENGLISH_NAME = 'Syriac'\n\n\nclass TAH:\n    ISO_639_1 = 'th'\n    ISO_639 = 'tah'\n    ENGLISH_NAME = 'Tahitian'\n\n\nclass TAM:\n    ISO_639_1 = ''\n    ISO_639 = 'tam'\n    ENGLISH_NAME = 'Tamil'\n\n\nclass TAT:\n    ISO_639_1 = ''\n    ISO_639 = 'tat'\n    ENGLISH_NAME = 'Tatar'\n\n\nclass TEL:\n    ISO_639_1 = 'te'\n    ISO_639 = 'tel'\n    ENGLISH_NAME = 'Telugu'\n\n\nclass TEM:\n    ISO_639_1 = ''\n    ISO_639 = 'tem'\n    ENGLISH_NAME = 'Timne'\n\n\nclass TER:\n    ISO_639_1 = ''\n    ISO_639 = 'ter'\n    ENGLISH_NAME = 'Tereno'\n\n\nclass TET:\n    ISO_639_1 = ''\n    ISO_639 = 'tet'\n    ENGLISH_NAME = 'Tetum'\n\n\nclass TGK:\n    ISO_639_1 = ''\n    ISO_639 = 'tgk'\n    ENGLISH_NAME = 'Tajik'\n\n\nclass TGL:\n    ISO_639_1 = ''\n    ISO_639 = 'tgl'\n    ENGLISH_NAME = 'Tagalog'\n\n\nclass THA:\n    ISO_639_1 = ''\n    ISO_639 = 'tha'\n    ENGLISH_NAME = 'Thai'\n\n\nclass TIB:\n    ISO_639_1 = ''\n    ISO_639 = 'tib'\n    ENGLISH_NAME = 'Tibetan'\n\n\nclass TIG:\n    ISO_639_1 = ''\n    ISO_639 = 'tig'\n    ENGLISH_NAME = 'Tigre'\n\n\nclass TIR:\n    ISO_639_1 = ''\n    ISO_639 = 'tir'\n    ENGLISH_NAME = 'Tigrinya'\n\n\nclass TIV:\n    ISO_639_1 = ''\n    ISO_639 = 'tiv'\n    ENGLISH_NAME = 'Tiv'\n\n\nclass TKL:\n    ISO_639_1 = ''\n    ISO_639 = 'tkl'\n    ENGLISH_NAME = 'Tokelau'\n\n\nclass TLH:\n    ISO_639_1 = ''\n    ISO_639 = 'tlh'\n    ENGLISH_NAME = 'Klingon'\n\n\nclass TLI:\n    ISO_639_1 = ''\n    ISO_639 = 'tli'\n    ENGLISH_NAME = 'Tlingit'\n\n\nclass TMH:\n    ISO_639_1 = ''\n    ISO_639 = 'tmh'\n    ENGLISH_NAME = 'Tamashek'\n\n\nclass TOG:\n    ISO_639_1 = ''\n    ISO_639 = 'tog'\n    ENGLISH_NAME = 'Tonga'\n\n\nclass TON:\n    ISO_639_1 = ''\n    ISO_639 = 'ton'\n    ENGLISH_NAME = 'Tonga'\n\n\nclass TPI:\n    ISO_639_1 = ''\n    ISO_639 = 'tpi'\n    ENGLISH_NAME = 'TokPisin'\n\n\nclass TSI:\n    ISO_639_1 = ''\n    ISO_639 = 'tsi'\n    ENGLISH_NAME = 'Tsimshian'\n\n\nclass TSN:\n    ISO_639_1 = ''\n    ISO_639 = 'tsn'\n    ENGLISH_NAME = 'Tswana'\n\n\nclass TSO:\n    ISO_639_1 = ''\n    ISO_639 = 'tso'\n    ENGLISH_NAME = 'Tsonga'\n\n\nclass TUK:\n    ISO_639_1 = ''\n    ISO_639 = 'tuk'\n    ENGLISH_NAME = 'Turkmen'\n\n\nclass TUM:\n    ISO_639_1 = ''\n    ISO_639 = 'tum'\n    ENGLISH_NAME = 'Tumbuka'\n\n\nclass TUR:\n    ISO_639_1 = ''\n    ISO_639 = 'tur'\n    ENGLISH_NAME = 'Turkish'\n\n\nclass TVL:\n    ISO_639_1 = ''\n    ISO_639 = 'tvl'\n    ENGLISH_NAME = 'Tuvalu'\n\n\nclass TWI:\n    ISO_639_1 = ''\n    ISO_639 = 'twi'\n    ENGLISH_NAME = 'Twi'\n\n\nclass TYV:\n    ISO_639_1 = ''\n    ISO_639 = 'tyv'\n    ENGLISH_NAME = 'Tuvinian'\n\n\nclass UDM:\n    ISO_639_1 = ''\n    ISO_639 = 'udm'\n    ENGLISH_NAME = 'Udmurt'\n\n\nclass UGA:\n    ISO_639_1 = ''\n    ISO_639 = 'uga'\n    ENGLISH_NAME = 'Ugaritic'\n\n\nclass UIG:\n    ISO_639_1 = ''\n    ISO_639 = 'uig'\n    ENGLISH_NAME = 'Uighur'\n\n\nclass UKR:\n    ISO_639_1 = ''\n    ISO_639 = 'ukr'\n    ENGLISH_NAME = 'Ukrainian'\n\n\nclass UMB:\n    ISO_639_1 = ''\n    ISO_639 = 'umb'\n    ENGLISH_NAME = 'Umbundu'\n\n\nclass UND:\n    ISO_639_1 = ''\n    ISO_639 = 'und'\n    ENGLISH_NAME = 'Undetermined'\n\n\nclass URD:\n    ISO_639_1 = ''\n    ISO_639 = 'urd'\n    ENGLISH_NAME = 'Urdu'\n\n\nclass UZB:\n    ISO_639_1 = ''\n    ISO_639 = 'uzb'\n    ENGLISH_NAME = 'Uzbek'\n\n\nclass VAI:\n    ISO_639_1 = ''\n    ISO_639 = 'vai'\n    ENGLISH_NAME = 'Vai'\n\n\nclass VEN:\n    ISO_639_1 = ''\n    ISO_639 = 'ven'\n    ENGLISH_NAME = 'Venda'\n\n\nclass VIE:\n    ISO_639_1 = ''\n    ISO_639 = 'vie'\n    ENGLISH_NAME = 'Vietnamese'\n\n\nclass VOL:\n    ISO_639_1 = ''\n    ISO_639 = 'vol'\n    ENGLISH_NAME = 'Volapük'\n\n\nclass VOT:\n    ISO_639_1 = ''\n    ISO_639 = 'vot'\n    ENGLISH_NAME = 'Votic'\n\n\nclass WAL:\n    ISO_639_1 = ''\n    ISO_639 = 'wal'\n    ENGLISH_NAME = 'Wolaitta'\n\n\nclass WAR:\n    ISO_639_1 = ''\n    ISO_639 = 'war'\n    ENGLISH_NAME = 'Waray'\n\n\nclass WAS:\n    ISO_639_1 = ''\n    ISO_639 = 'was'\n    ENGLISH_NAME = 'Washo'\n\n\nclass WEL:\n    ISO_639_1 = ''\n    ISO_639 = 'wel'\n    ENGLISH_NAME = 'Welsh'\n\n\nclass WLN:\n    ISO_639_1 = ''\n    ISO_639 = 'wln'\n    ENGLISH_NAME = 'Walloon'\n\n\nclass WOL:\n    ISO_639_1 = ''\n    ISO_639 = 'wol'\n    ENGLISH_NAME = 'Wolof'\n\n\nclass XAL:\n    ISO_639_1 = ''\n    ISO_639 = 'xal'\n    ENGLISH_NAME = 'Kalmyk'\n\n\nclass XHO:\n    ISO_639_1 = ''\n    ISO_639 = 'xho'\n    ENGLISH_NAME = 'Xhosa'\n\n\nclass YAO:\n    ISO_639_1 = ''\n    ISO_639 = 'yao'\n    ENGLISH_NAME = 'Yao'\n\n\nclass YAP:\n    ISO_639_1 = ''\n    ISO_639 = 'yap'\n    ENGLISH_NAME = 'Yapese'\n\n\nclass YID:\n    ISO_639_1 = ''\n    ISO_639 = 'yid'\n    ENGLISH_NAME = 'Yiddish'\n\n\nclass YOR:\n    ISO_639_1 = ''\n    ISO_639 = 'yor'\n    ENGLISH_NAME = 'Yoruba'\n\n\nclass ZAP:\n    ISO_639_1 = ''\n    ISO_639 = 'zap'\n    ENGLISH_NAME = 'Zapotec'\n\n\nclass ZBL:\n    ISO_639_1 = ''\n    ISO_639 = 'zbl'\n    ENGLISH_NAME = 'Blissymbols'\n\n\nclass ZEN:\n    ISO_639_1 = ''\n    ISO_639 = 'zen'\n    ENGLISH_NAME = 'Zenaga'\n\n\nclass ZGH:\n    ISO_639_1 = ''\n    ISO_639 = 'zgh'\n    ENGLISH_NAME = 'StandardMoroccanTamazight'\n\n\nclass ZHA:\n    ISO_639_1 = ''\n    ISO_639 = 'zha'\n    ENGLISH_NAME = 'Zhuang'\n\n\nclass ZHS:\n    ISO_639_1 = ''\n    ISO_639 = 'zhs'\n    ENGLISH_NAME = 'SimplifiedChinese'\n\n\nclass ZHT:\n    ISO_639_1 = ''\n    ISO_639 = 'zht'\n    ENGLISH_NAME = 'TraditionalChinese'\n\n\nclass ZUL:\n    ISO_639_1 = ''\n    ISO_639 = 'zul'\n    ENGLISH_NAME = 'Zulu'\n\n\nclass ZUN:\n    ISO_639_1 = ''\n    ISO_639 = 'zun'\n    ENGLISH_NAME = 'Zuni'\n\n\nclass ZZA:\n    ISO_639_1 = ''\n    ISO_639 = 'zza'\n    ENGLISH_NAME = 'Zaza'\n\n\ndef get_language_classes():\n    return inspect.getmembers(sys.modules[__name__], inspect.isclass)\n"
  },
  {
    "path": "chatterbot/logic/__init__.py",
    "content": "from chatterbot.logic.logic_adapter import LogicAdapter\nfrom chatterbot.logic.best_match import BestMatch\nfrom chatterbot.logic.mathematical_evaluation import MathematicalEvaluation\nfrom chatterbot.logic.specific_response import SpecificResponseAdapter\nfrom chatterbot.logic.time_adapter import TimeLogicAdapter\nfrom chatterbot.logic.unit_conversion import UnitConversion\nfrom chatterbot.logic.llm_adapters import (\n    LLMLogicAdapter,\n    OllamaLogicAdapter,\n    OpenAILogicAdapter,\n)\n\n\n__all__ = (\n    'LogicAdapter',\n    'BestMatch',\n    'MathematicalEvaluation',\n    'SpecificResponseAdapter',\n    'TimeLogicAdapter',\n    'UnitConversion',\n    'LLMLogicAdapter',\n    'OllamaLogicAdapter',\n    'OpenAILogicAdapter',\n)\n"
  },
  {
    "path": "chatterbot/logic/best_match.py",
    "content": "from chatterbot.logic import LogicAdapter\nfrom chatterbot.conversation import Statement\nfrom chatterbot import filters\n\n\nclass BestMatch(LogicAdapter):\n    \"\"\"\n    A logic adapter that returns a response based on known responses to\n    the closest matches to the input statement.\n\n    :param excluded_words:\n        The excluded_words parameter allows a list of words to be set that will\n        prevent the logic adapter from returning statements that have text\n        containing any of those words. This can be useful for preventing your\n        chat bot from saying swears when it is being demonstrated in front of\n        an audience.\n        Defaults to None\n    :type excluded_words: list\n    \"\"\"\n\n    def __init__(self, chatbot, **kwargs):\n        super().__init__(chatbot, **kwargs)\n\n        self.excluded_words = kwargs.get('excluded_words')\n\n    def process(self, input_statement: Statement, additional_response_selection_parameters=None) -> Statement:\n\n        # Get all statements that have a response text similar to the input statement\n        search_results = self.search_algorithm.search(input_statement)\n\n        # Use the input statement as the closest match if no other results are found\n        input_statement.confidence = 0  # Use 0 confidence when no other results are found\n        closest_match = input_statement\n\n        # Search for the closest match to the input statement\n        for result in search_results:\n            closest_match = result\n\n            # Stop searching if a match that is close enough is found\n            if result.confidence >= self.maximum_similarity_threshold:\n                break\n\n        self.chatbot.logger.info('Selecting \"{}\" as a response to \"{}\" with a confidence of {}'.format(\n            closest_match.text, input_statement.text, closest_match.confidence\n        ))\n\n        # Semantic vector search vs indexed text search have different architectures:\n        #\n        # For SQL with indexed text search:\n        #   - Phase 1 finds a match based on string similarity (Levenshtein distance)\n        #   - Phase 2 finds variations of that match to get diverse responses\n        #   - This makes sense because you might have multiple instances of similar statements\n        #     learned from different conversations that provide different response options\n        #\n        # For Redis with semantic vectors:\n        #   - Phase 1 finds semantically similar responses using vector embeddings\n        #   - The semantic similarity already captures the \"closeness\" we want\n        #   - Phase 2 would be redundant - we already have the best semantic match\n        #   - The vector search inherently considers the entire semantic space, not just\n        #     exact string matches, so additional variation searching is unnecessary\n        #\n        # NOTE: This difference of functionality may need to be modified in the future\n        # if the redis adapter is determined to benefit from a Phase 2 style response\n        # selection. The main symptom that would drive such a change would be low\n        # quality or repetitive responses when using semantic vector search.\n        #\n        # Therefore, semantic vector search returns the Phase 1 result directly.\n        if self.search_algorithm.name == 'semantic_vector_search' and closest_match.confidence > 0:\n            response = closest_match\n            self.chatbot.logger.info('Using semantic search result directly: \"{}\"'.format(response.text))\n        else:\n            # For other search algorithms (indexed_text_search, text_search),\n            # we need to find responses to the closest match\n            recent_repeated_responses = filters.get_recent_repeated_responses(\n                self.chatbot,\n                input_statement.conversation\n            )\n\n            for index, recent_repeated_response in enumerate(recent_repeated_responses):\n                self.chatbot.logger.info('{}. Excluding recent repeated response of \"{}\"'.format(\n                    index, recent_repeated_response\n                ))\n\n            response_selection_parameters = {\n                'search_text': closest_match.search_text,\n                'persona_not_startswith': 'bot:',\n                'exclude_text': recent_repeated_responses,\n                'exclude_text_words': self.excluded_words\n            }\n\n            alternate_response_selection_parameters = {\n                'search_in_response_to': input_statement.search_text or self.chatbot.tagger.get_text_index_string(\n                    input_statement.text\n                ),\n                'persona_not_startswith': 'bot:',\n                'exclude_text': recent_repeated_responses,\n                'exclude_text_words': self.excluded_words\n            }\n\n            if additional_response_selection_parameters:\n                response_selection_parameters.update(\n                    additional_response_selection_parameters\n                )\n                alternate_response_selection_parameters.update(\n                    additional_response_selection_parameters\n                )\n\n            # Get all statements with text similar to the closest match\n            response_list = list(self.chatbot.storage.filter(**response_selection_parameters))\n\n            if response_list:\n                response = self.select_response(\n                    input_statement,\n                    response_list,\n                    self.chatbot.storage\n                )\n\n                response.confidence = closest_match.confidence\n                self.chatbot.logger.info('Selecting \"{}\" from {} optimal responses.'.format(\n                    response.text,\n                    len(response_list)\n                ))\n            else:\n                '''\n                The case where there was no responses returned for the selected match\n                but a value exists for the statement the match is in response to.\n                '''\n                self.chatbot.logger.info('No responses found. Generating alternate response list.')\n\n                alternate_response_list = list(self.chatbot.storage.filter(\n                    **alternate_response_selection_parameters\n                ))\n\n                if alternate_response_list:\n                    response = self.select_response(\n                        input_statement,\n                        alternate_response_list,\n                        self.chatbot.storage\n                    )\n\n                    response.confidence = closest_match.confidence\n                    self.chatbot.logger.info('Selected alternative response \"{}\" from {} options'.format(\n                        response.text,\n                        len(alternate_response_list)\n                    ))\n                else:\n                    response = self.get_default_response(input_statement)\n                    self.chatbot.logger.info('Using \"%s\" as a default response.', response.text)\n\n        return response\n"
  },
  {
    "path": "chatterbot/logic/llm_adapters.py",
    "content": "\"\"\"\nLLM Logic Adapters for ChatterBot.\n\nThis module provides logic adapters that integrate Large Language Models.\nLLM adapters can use other logic adapters as tools via MCP (Model Context Protocol).\n\"\"\"\nimport json\nfrom typing import Any, Dict, List, Optional, Union\n\nfrom chatterbot.logic.logic_adapter import LogicAdapter\nfrom chatterbot.conversation import Statement\nfrom chatterbot.logic.mcp_tools import (\n    is_tool_adapter,\n    convert_to_openai_tool_format,\n    convert_to_ollama_tool_format\n)\nfrom chatterbot import utils\n\n\nclass LLMLogicAdapter(LogicAdapter):\n    \"\"\"\n    Base class for Large Language Model logic adapters.\n\n    .. warning::\n        LLM logic adapters are experimental and may change in future releases.\n        Tool calling functionality is still being refined and may have limitations.\n\n    LLM adapters can participate in ChatterBot's consensus voting mechanism\n    alongside traditional logic adapters. They can also use other logic\n    adapters as tools through MCP.\n\n    Configuration parameters:\n        model (str): The LLM model name (required)\n        host (str): API endpoint URL (optional, provider-specific default)\n        logic_adapters_as_tools (list): List of logic adapters to expose as tools\n        force_native_tools (bool): Force native tool calling (None=auto-detect)\n        min_confidence (float): Minimum confidence for LLM responses (default: 0.5)\n        max_confidence (float): Maximum confidence for LLM responses (default: 0.85)\n        conversation_context_count (int): Number of previous statements to include (default: 5)\n        system_message (str): Custom system message for the LLM\n\n    Example:\n        {\n            'import_path': 'chatterbot.logic.OllamaLogicAdapter',\n            'model': 'llama3.1',\n            'logic_adapters_as_tools': [\n                'chatterbot.logic.MathematicalEvaluation',\n                'chatterbot.logic.TimeLogicAdapter'\n            ],\n            'min_confidence': 0.6,\n            'max_confidence': 0.9\n        }\n    \"\"\"\n\n    def __init__(self, chatbot, **kwargs):\n        super().__init__(chatbot, **kwargs)\n\n        # Model configuration\n        self.model = kwargs.get('model')\n        if not self.model:\n            raise ValueError(\"LLM logic adapters require a 'model' parameter\")\n\n        self.host = kwargs.get('host')\n\n        # Confidence range for LLM responses (for consensus voting)\n        self.min_confidence = kwargs.get('min_confidence', 0.5)\n        self.max_confidence = kwargs.get('max_confidence', 0.85)\n\n        # Conversation context\n        self.conversation_context_count = kwargs.get('conversation_context_count', 5)\n\n        # System message\n        default_system_message = (\n            \"You are a helpful AI assistant engaged in a direct conversation. \"\n            \"Address the person you're speaking with directly rather than referring to them in third person. \"\n            \"Please keep responses concise, conversational, and under 1100 tokens.\"\n        )\n\n        # If tools are configured, enhance system message to clarify tool usage\n        if kwargs.get('logic_adapters_as_tools'):\n            default_system_message += (\n                \"\\n\\nYou have access to specialized tools that can help you answer certain types of questions. \"\n                \"Use these tools when they would be helpful, but you should respond naturally to ALL questions, \"\n                \"not just tool-related ones. For general conversation, greetings, or topics outside the tools' scope, \"\n                \"respond directly without using tools.\"\n            )\n\n        self.system_message = kwargs.get('system_message', default_system_message)\n\n        # Tool calling configuration\n        self.force_native_tools = kwargs.get('force_native_tools', None)\n        self.tool_registry = {}\n        self._native_tools_supported = None  # Cached tool capability detection result\n\n        # Initialize tool adapters if provided\n        logic_adapters_as_tools = kwargs.get('logic_adapters_as_tools', [])\n        if logic_adapters_as_tools:\n            self._initialize_tool_adapters(logic_adapters_as_tools, **kwargs)\n            # Detect tool capability once during initialization\n            self._native_tools_supported = self._detect_tool_capability()\n\n    def _initialize_tool_adapters(self, adapter_configs: List[Union[str, Dict]], **kwargs):\n        \"\"\"\n        Initialize logic adapters to be used as tools.\n\n        Args:\n            adapter_configs: List of adapter import paths or config dicts\n            **kwargs: Additional kwargs to pass to adapters\n        \"\"\"\n        for adapter_config in adapter_configs:\n            # Validate and initialize the adapter\n            utils.validate_adapter_class(adapter_config, LogicAdapter)\n            adapter = utils.initialize_class(adapter_config, self.chatbot, **kwargs)\n\n            # Check if adapter supports tool functionality\n            if is_tool_adapter(adapter):\n                tool_name = adapter.get_tool_name()\n                self.tool_registry[tool_name] = adapter\n                self.chatbot.logger.info(\n                    f\"Registered tool: {tool_name} from {adapter.__class__.__name__}\"\n                )\n            else:\n                self.chatbot.logger.warning(\n                    f\"Adapter {adapter.__class__.__name__} does not implement MCPToolAdapter, skipping\"\n                )\n\n    def _get_conversation_context(self, input_statement: Statement) -> List[Dict[str, str]]:\n        \"\"\"\n        Retrieve previous conversation context from storage.\n\n        .. note::\n            Security Note: Conversation history is loaded from storage without modification.\n            If you need to scan historical messages for security issues (e.g., context poisoning),\n            override this method in a base class.\n\n        Args:\n            input_statement: The current input statement\n\n        Returns:\n            List of message dicts in LLM format\n        \"\"\"\n        messages = []\n\n        if not input_statement.conversation:\n            return messages\n\n        try:\n            # Query storage for recent statements in this conversation\n            previous_statements = self.chatbot.storage.filter(\n                conversation=input_statement.conversation,\n                order_by=['id'],\n                page_size=self.conversation_context_count * 2  # x2 to account for bot responses\n            )\n\n            # Convert to LLM message format\n            for stmt in previous_statements:\n                # Determine role based on persona\n                if stmt.persona and stmt.persona.startswith('bot:'):\n                    role = 'assistant'\n                else:\n                    role = 'user'\n\n                messages.append({\n                    'role': role,\n                    'content': stmt.text\n                })\n\n        except Exception as e:\n            self.chatbot.logger.warning(f\"Failed to retrieve conversation context: {e}\")\n\n        return messages\n\n    def _build_base_messages(self, input_statement: Statement, system_message: Optional[str] = None) -> List[Dict[str, str]]:\n        \"\"\"\n        Build base message list for LLM API calls.\n\n        Args:\n            input_statement: The input statement\n            system_message: Optional system message override\n\n        Returns:\n            List of message dicts in LLM format\n        \"\"\"\n        messages = [{'role': 'system', 'content': system_message or self.system_message}]\n        messages.extend(self._get_conversation_context(input_statement))\n        messages.append({'role': 'user', 'content': input_statement.text})\n        return messages\n\n    def _format_error_response(self, error: Exception) -> str:\n        \"\"\"\n        Format a consistent error response message.\n\n        Args:\n            error: The exception that occurred\n\n        Returns:\n            Formatted error message string\n        \"\"\"\n        return f\"I apologize, but I encountered an error: {str(error)}\"\n\n    def _supports_native_tools(self) -> bool:\n        \"\"\"\n        Determine if the current model supports native tool calling.\n\n        Returns:\n            True if native tools are supported\n        \"\"\"\n        # If user explicitly set force_native_tools, use that\n        if self.force_native_tools is not None:\n            return self.force_native_tools\n\n        # Otherwise, use cached detection result\n        # (detection happens once during initialization)\n        if self._native_tools_supported is None:\n            # Fallback: detect now if somehow not set during init\n            self._native_tools_supported = self._detect_tool_capability()\n\n        return self._native_tools_supported\n\n    def _detect_tool_capability(self) -> bool:\n        \"\"\"\n        Detect if the model supports native tool calling.\n        Override in subclasses for provider-specific detection.\n\n        Returns:\n            True if tools are supported\n        \"\"\"\n        return False\n\n    def _get_tools_for_llm(self) -> List[Dict[str, Any]]:\n        \"\"\"\n        Get tool definitions in the format expected by the LLM provider.\n        Override in subclasses for provider-specific formats.\n\n        Returns:\n            List of tool definitions\n        \"\"\"\n        raise NotImplementedError(\"Subclasses must implement _get_tools_for_llm()\")\n\n    def _execute_tool(self, tool_name: str, parameters: Dict[str, Any]) -> str:\n        \"\"\"\n        Execute a tool by its name with the given parameters.\n\n        Args:\n            tool_name: Name of the tool to execute\n            parameters: Tool parameters\n\n        Returns:\n            Tool execution result as string\n        \"\"\"\n        if tool_name not in self.tool_registry:\n            self.chatbot.logger.warning(f\"Tool not found: '{tool_name}'\")\n            return f\"Error: Tool '{tool_name}' not found\"\n\n        adapter = self.tool_registry[tool_name]\n\n        try:\n            # Validate parameters\n            if not adapter.validate_tool_parameters(**parameters):\n                self.chatbot.logger.warning(f\"Invalid parameters for tool '{tool_name}': {parameters}\")\n                return f\"Error: Invalid parameters for tool '{tool_name}'\"\n\n            # Log tool execution\n            self.chatbot.logger.info(f\"Executing tool: '{tool_name}' with parameters: {parameters}\")\n\n            # Execute tool\n            result = adapter.execute_as_tool(**parameters)\n\n            # Convert result to string if needed\n            if not isinstance(result, str):\n                result = str(result)\n\n            self.chatbot.logger.info(f\"Tool '{tool_name}' completed successfully\")\n            return result\n\n        except Exception as e:\n            self.chatbot.logger.error(f\"Tool execution error for '{tool_name}': {e}\")\n            return f\"Error executing tool '{tool_name}': {str(e)}\"\n\n    def _handle_native_tool_calling(self, input_statement: Statement) -> Statement:\n        \"\"\"\n        Handle tool calling with native LLM support.\n        Override in subclasses for provider-specific implementation.\n\n        Args:\n            input_statement: The input statement to process\n\n        Returns:\n            Response statement with confidence\n        \"\"\"\n        raise NotImplementedError(\"Subclasses must implement _handle_native_tool_calling()\")\n\n    def _handle_prompt_based_tool_calling(self, input_statement: Statement) -> Statement:\n        \"\"\"\n        Handle tool calling via prompt engineering for models without native support.\n\n        This method guides the LLM to output structured JSON that can be parsed\n        and routed to appropriate tools.\n\n        Args:\n            input_statement: The input statement to process\n\n        Returns:\n            Response statement with confidence\n        \"\"\"\n        # Build tool descriptions for prompt\n        tool_descriptions = []\n        for adapter in self.tool_registry.values():\n            schema = adapter.get_tool_schema()\n            tool_desc = f\"- {schema['name']}: {schema['description']}\"\n            tool_descriptions.append(tool_desc)\n\n        tools_text = \"\\n\".join(tool_descriptions)\n\n        # TODO: Consider switching from JSON to TOON\n\n        # Enhanced system message with tool instructions\n        system_msg = f\"\"\"{self.system_message}\n\nYou have access to the following specialized tools:\n{tools_text}\n\nIMPORTANT: You can respond to ANY question the user asks. Use tools when they would be helpful for specific tasks, but respond naturally to general conversation, greetings, or topics that don't require tools.\n\nWhen you need to use a tool, respond with a JSON object in this exact format:\n{{\"tool\": \"tool_name\", \"parameters\": {{\"param1\": \"value1\"}}}}\n\nFor all other questions, respond normally with plain text conversationally.\"\"\"\n\n        # Get LLM response\n        response_text = self._call_llm(input_statement, system_msg)\n\n        # Try to parse as JSON (tool call)\n        if response_text.strip().startswith('{'):\n            try:\n                tool_call = json.loads(response_text)\n                tool_name = tool_call.get('tool')\n                parameters = tool_call.get('parameters', {})\n\n                self.chatbot.logger.info(f\"LLM requested tool via prompt: '{tool_name}'\")\n\n                # Execute tool\n                tool_result = self._execute_tool(tool_name, parameters)\n\n                # Get final response from LLM with tool result\n                followup_msg = f\"Tool '{tool_name}' returned: {tool_result}\\nProvide a natural language response to the user.\"\n                final_response = self._call_llm_with_context(input_statement, followup_msg)\n\n                response = Statement(text=final_response)\n                response.confidence = self._calculate_confidence(final_response)\n                return response\n\n            except json.JSONDecodeError:\n                pass  # Not a tool call, treat as normal response\n\n        # Regular text response\n        response = Statement(text=response_text)\n        response.confidence = self._calculate_confidence(response_text)\n        return response\n\n    def _call_llm(self, input_statement: Statement, system_message: Optional[str] = None) -> str:\n        \"\"\"\n        Make a direct LLM API call without tool support.\n        Override in subclasses for provider-specific implementation.\n\n        Args:\n            input_statement: The input statement\n            system_message: Optional system message override\n\n        Returns:\n            LLM response text\n        \"\"\"\n        raise NotImplementedError(\"Subclasses must implement _call_llm()\")\n\n    def _call_llm_with_context(self, input_statement: Statement, additional_context: str) -> str:\n        \"\"\"\n        Make an LLM call with additional context message.\n\n        Args:\n            input_statement: The input statement\n            additional_context: Additional context to include\n\n        Returns:\n            LLM response text\n        \"\"\"\n        # This will be implemented in subclasses using their specific API\n        raise NotImplementedError(\"Subclasses must implement _call_llm_with_context()\")\n\n    def _calculate_confidence(self, response_text: str) -> float:\n        \"\"\"\n        Calculate confidence score for LLM response.\n\n        Uses a simple heuristic based on response length and quality indicators.\n        Returns a value between min_confidence and max_confidence.\n\n        Args:\n            response_text: The LLM's response text\n\n        Returns:\n            Confidence score between 0 and 1\n        \"\"\"\n        # Base confidence (middle of range)\n        confidence = (self.min_confidence + self.max_confidence) / 2\n\n        # Adjust based on response length (very short or very long may be less reliable)\n        length = len(response_text)\n        if length < 10:\n            confidence -= 0.1\n        elif 50 < length < 200:\n            confidence += 0.05\n\n        # Clamp to configured range\n        confidence = max(self.min_confidence, min(self.max_confidence, confidence))\n\n        return confidence\n\n    def process(self, statement: Statement, additional_response_selection_parameters: dict = None) -> Statement:\n        \"\"\"\n        Process the input statement using the LLM.\n\n        Args:\n            statement: The input statement to process\n            additional_response_selection_parameters: Additional parameters (unused)\n\n        Returns:\n            Response statement with confidence score\n        \"\"\"\n        # If no tools are configured, just call LLM directly\n        if not self.tool_registry:\n            response_text = self._call_llm(statement)\n            response = Statement(text=response_text)\n            response.confidence = self._calculate_confidence(response_text)\n            return response\n\n        # Determine tool calling method\n        if self._supports_native_tools():\n            return self._handle_native_tool_calling(statement)\n        else:\n            return self._handle_prompt_based_tool_calling(statement)\n\n\nclass OllamaLogicAdapter(LLMLogicAdapter):\n    \"\"\"\n    Logic adapter for Ollama LLMs with MCP tool support.\n\n    .. warning::\n        This adapter is experimental. Tool capability detection uses template\n        inspection which may not work for all model formats. Tool calling behavior\n        varies significantly between models.\n\n    Configuration:\n        model (str): Ollama model name (e.g., 'llama3.1', 'mistral')\n        host (str): Ollama API endpoint (default: http://localhost:11434)\n        logic_adapters_as_tools (list): Logic adapters to expose as tools\n\n    Example:\n        {\n            'import_path': 'chatterbot.logic.OllamaLogicAdapter',\n            'model': 'llama3.1',\n            'host': 'http://localhost:11434',\n            'logic_adapters_as_tools': [\n                'chatterbot.logic.MathematicalEvaluation',\n                'chatterbot.logic.TimeLogicAdapter'\n            ]\n        }\n    \"\"\"\n\n    def __init__(self, chatbot, **kwargs):\n        # Set default host before parent init\n        if 'host' not in kwargs:\n            kwargs['host'] = 'http://localhost:11434'\n\n        super().__init__(chatbot, **kwargs)\n\n        # Initialize Ollama client\n        try:\n            from ollama import Client\n            self.client = Client(host=self.host)\n        except ImportError:\n            raise ImportError(\n                \"Ollama library not installed. Install with: pip install chatterbot[dev]\"\n            )\n\n    def _detect_tool_capability(self) -> bool:\n        \"\"\"\n        Detect if the Ollama model supports native tool calling.\n\n        Uses a combination of known model patterns and template inspection\n        to determine tool support.\n\n        Returns:\n            True if model supports tools\n        \"\"\"\n        # Known models with tool support (as of 2026)\n        # Check model name patterns - handles versioned models (e.g., llama3.1:8b)\n        model_base = self.model.split(':')[0].lower()\n\n        # Known tool-supporting model patterns\n        tool_supporting_patterns = [\n            # Llama series\n            'llama3.1', 'llama3.2', 'llama3-groq-tool',\n            # Mistral series\n            'mistral', 'mistral-nemo', 'mistral-large',\n            # Qwen series\n            'qwen2.5', 'qwen2.5-coder',\n            # Specialized models\n            'firefunction', 'nemotron', 'command-r', 'command-r-plus',\n            # Enterprise models\n            'granite3.1-dense', 'hermes3'\n        ]\n\n        # Check if model matches any known pattern\n        for pattern in tool_supporting_patterns:\n            if pattern in model_base:\n                self.chatbot.logger.info(\n                    f\"Model '{self.model}' supports native tool calling (known model)\"\n                )\n                return True\n\n        # Fallback to template inspection for unknown models\n        try:\n            # Get model metadata\n            model_info = self.client.show(self.model)\n\n            # Get the template string\n            template = model_info.get('template', '')\n\n            # Check for tool-specific tokens in the template\n            has_tools = '{{ .Tools }}' in template or '{{ tools }}' in template\n\n            if has_tools:\n                self.chatbot.logger.info(\n                    f\"Model '{self.model}' supports native tool calling (template inspection)\"\n                )\n            else:\n                self.chatbot.logger.info(\n                    f\"Model '{self.model}' does not support native tool calling, will use prompt-based approach\"\n                )\n\n            return has_tools\n\n        except Exception as e:\n            self.chatbot.logger.warning(\n                f\"Failed to inspect model '{self.model}' for tool support: {e}. \"\n                f\"Falling back to prompt-based tool calling.\"\n            )\n            return False\n\n    def _get_tools_for_llm(self) -> List[Dict[str, Any]]:\n        \"\"\"\n        Get tool definitions in Ollama format.\n\n        Returns:\n            List of Ollama-formatted tool definitions\n        \"\"\"\n        tools = []\n        for adapter in self.tool_registry.values():\n            schema = adapter.get_tool_schema()\n            ollama_tool = convert_to_ollama_tool_format(schema)\n            tools.append(ollama_tool)\n        return tools\n\n    def _call_llm(self, input_statement: Statement, system_message: Optional[str] = None) -> str:\n        \"\"\"\n        Call Ollama API without tool support.\n\n        Args:\n            input_statement: The input statement\n            system_message: Optional system message override\n\n        Returns:\n            LLM response text\n        \"\"\"\n        # Build messages with conversation context\n        messages = self._build_base_messages(input_statement, system_message)\n\n        try:\n            response = self.client.chat(\n                model=self.model,\n                messages=messages\n            )\n            return response['message']['content']\n        except Exception as e:\n            self.chatbot.logger.error(f\"Ollama API error: {e}\")\n            return self._format_error_response(e)\n\n    def _call_llm_with_context(self, input_statement: Statement, additional_context: str) -> str:\n        \"\"\"\n        Call Ollama with additional context for tool result processing.\n\n        Args:\n            input_statement: The input statement\n            additional_context: Additional context message\n\n        Returns:\n            LLM response text\n        \"\"\"\n        messages = self._build_base_messages(input_statement)\n        messages.append({'role': 'assistant', 'content': additional_context})\n\n        try:\n            response = self.client.chat(\n                model=self.model,\n                messages=messages\n            )\n            return response['message']['content']\n        except Exception as e:\n            self.chatbot.logger.error(f\"Ollama API error: {e}\")\n            return self._format_error_response(e)\n\n    def _handle_native_tool_calling(self, input_statement: Statement) -> Statement:\n        \"\"\"\n        Handle tool calling with Ollama's native function calling support.\n\n        Args:\n            input_statement: The input statement to process\n\n        Returns:\n            Response statement with confidence\n        \"\"\"\n        # Build messages\n        messages = self._build_base_messages(input_statement)\n\n        # Get tools in Ollama format\n        tools = self._get_tools_for_llm()\n\n        # TODO: Look into support for thinking mode\n\n        try:\n            # Initial LLM call with tools\n            response = self.client.chat(\n                model=self.model,\n                messages=messages,\n                tools=tools\n            )\n\n            message = response['message']\n\n            # Check if LLM wants to use a tool\n            if tool_calls := message.get('tool_calls'):\n                self.chatbot.logger.info(f\"Ollama LLM requested {len(tool_calls)} tool(s)\")\n\n                # Serialize the message properly for Ollama API\n                # The message object needs to be converted to dict format\n                if hasattr(message, 'model_dump'):\n                    # Pydantic v2\n                    message_dict = message.model_dump(exclude_none=True)\n                elif hasattr(message, 'dict'):\n                    # Pydantic v1\n                    message_dict = message.dict(exclude_none=True)\n                else:\n                    # Fallback if it's already a dict or needs manual conversion\n                    message_dict = dict(message) if not isinstance(message, dict) else message\n\n                messages.append(message_dict)\n\n                # Execute each tool call and add results\n                for tool_call in tool_calls:\n                    function = tool_call['function']\n                    tool_name = function['name']\n                    parameters = function.get('arguments', {})\n\n                    # Execute tool\n                    tool_result = self._execute_tool(tool_name, parameters)\n\n                    # Add tool result to conversation with tool_name field\n                    messages.append({\n                        'role': 'tool',\n                        'content': tool_result,\n                        'tool_name': tool_name\n                    })\n\n                # Get final response from LLM with tool results\n                final_response = self.client.chat(\n                    model=self.model,\n                    messages=messages,\n                    tools=tools\n                )\n\n                response_text = final_response['message']['content']\n            else:\n                # No tool call, use direct response\n                response_text = message['content']\n\n            response = Statement(text=response_text)\n            response.confidence = self._calculate_confidence(response_text)\n            return response\n\n        except Exception as e:\n            self.chatbot.logger.error(f\"Ollama tool calling error: {e}\")\n            response = Statement(text=self._format_error_response(e))\n            response.confidence = self.min_confidence\n            return response\n\n\nclass OpenAILogicAdapter(LLMLogicAdapter):\n    \"\"\"\n    Logic adapter for OpenAI LLMs with MCP tool support.\n\n    .. warning::\n        This adapter is experimental.\n\n    Configuration:\n        model (str): OpenAI model name (e.g., 'gpt-4', 'gpt-3.5-turbo')\n        host (str): Optional custom API endpoint\n        logic_adapters_as_tools (list): Logic adapters to expose as tools\n\n    Environment:\n        OPENAI_API_KEY: Required for authentication\n\n    Example:\n        {\n            'import_path': 'chatterbot.logic.OpenAILogicAdapter',\n            'model': 'gpt-4o-mini',\n            'logic_adapters_as_tools': [\n                'chatterbot.logic.MathematicalEvaluation',\n                'chatterbot.logic.TimeLogicAdapter'\n            ]\n        }\n    \"\"\"\n\n    def __init__(self, chatbot, **kwargs):\n        super().__init__(chatbot, **kwargs)\n\n        # Initialize OpenAI client\n        try:\n            from openai import OpenAI as OpenAIClient\n            if self.host:\n                self.client = OpenAIClient(base_url=self.host)\n            else:\n                self.client = OpenAIClient()\n        except ImportError:\n            raise ImportError(\n                \"OpenAI library not installed. Install with: pip install chatterbot[dev]\"\n            )\n\n    def _detect_tool_capability(self) -> bool:\n        \"\"\"\n        Detect if the OpenAI model supports tool calling.\n\n        Returns:\n            True (all current OpenAI models support tool calling)\n        \"\"\"\n        return True\n\n    def _get_tools_for_llm(self) -> List[Dict[str, Any]]:\n        \"\"\"\n        Get tool definitions in OpenAI format.\n\n        Returns:\n            List of OpenAI-formatted tool definitions\n        \"\"\"\n        tools = []\n        for adapter in self.tool_registry.values():\n            schema = adapter.get_tool_schema()\n            openai_tool = convert_to_openai_tool_format(schema)\n            tools.append(openai_tool)\n        return tools\n\n    def _call_llm(self, input_statement: Statement, system_message: Optional[str] = None) -> str:\n        \"\"\"\n        Call OpenAI API without tool support.\n\n        Args:\n            input_statement: The input statement\n            system_message: Optional system message override\n\n        Returns:\n            LLM response text\n        \"\"\"\n        # Build messages with conversation context\n        messages = self._build_base_messages(input_statement, system_message)\n\n        try:\n            response = self.client.chat.completions.create(\n                model=self.model,\n                messages=messages\n            )\n            return response.choices[0].message.content\n        except Exception as e:\n            self.chatbot.logger.error(f\"OpenAI API error: {e}\")\n            return self._format_error_response(e)\n\n    def _call_llm_with_context(self, input_statement: Statement, additional_context: str) -> str:\n        \"\"\"\n        Call OpenAI with additional context for tool result processing.\n\n        Args:\n            input_statement: The input statement\n            additional_context: Additional context message\n\n        Returns:\n            LLM response text\n        \"\"\"\n        messages = self._build_base_messages(input_statement)\n        messages.append({'role': 'assistant', 'content': additional_context})\n\n        try:\n            response = self.client.chat.completions.create(\n                model=self.model,\n                messages=messages\n            )\n            return response.choices[0].message.content\n        except Exception as e:\n            self.chatbot.logger.error(f\"OpenAI API error: {e}\")\n            return self._format_error_response(e)\n\n    def _handle_native_tool_calling(self, input_statement: Statement) -> Statement:\n        \"\"\"\n        Handle tool calling with OpenAI's native function calling support.\n\n        Args:\n            input_statement: The input statement to process\n\n        Returns:\n            Response statement with confidence\n        \"\"\"\n        # Build messages\n        messages = self._build_base_messages(input_statement)\n\n        # Get tools in OpenAI format\n        tools = self._get_tools_for_llm()\n\n        try:\n            # Initial LLM call with tools\n            response = self.client.chat.completions.create(\n                model=self.model,\n                messages=messages,\n                tools=tools\n            )\n\n            message = response.choices[0].message\n\n            # Check if LLM wants to use a tool\n            if tool_calls := message.tool_calls:\n                self.chatbot.logger.info(f\"OpenAI LLM requested {len(tool_calls)} tool(s)\")\n                # Execute each tool call\n                for tool_call in tool_calls:\n                    function = tool_call.function\n                    tool_name = function.name\n                    parameters = json.loads(function.arguments)\n\n                    # Execute tool\n                    tool_result = self._execute_tool(tool_name, parameters)\n\n                    # Add assistant message with tool call\n                    messages.append({\n                        'role': 'assistant',\n                        'content': None,\n                        'tool_calls': [{\n                            'id': tool_call.id,\n                            'type': 'function',\n                            'function': {\n                                'name': tool_name,\n                                'arguments': function.arguments\n                            }\n                        }]\n                    })\n\n                    # Add tool result message\n                    messages.append({\n                        'role': 'tool',\n                        'tool_call_id': tool_call.id,\n                        'content': tool_result\n                    })\n\n                # Get final response from LLM with tool results\n                final_response = self.client.chat.completions.create(\n                    model=self.model,\n                    messages=messages,\n                    tools=tools\n                )\n\n                response_text = final_response.choices[0].message.content\n            else:\n                # No tool call, use direct response\n                response_text = message.content\n\n            response = Statement(text=response_text)\n            response.confidence = self._calculate_confidence(response_text)\n            return response\n\n        except Exception as e:\n            self.chatbot.logger.error(f\"OpenAI tool calling error: {e}\")\n            response = Statement(text=self._format_error_response(e))\n            response.confidence = self.min_confidence\n            return response\n"
  },
  {
    "path": "chatterbot/logic/logic_adapter.py",
    "content": "from random import choice\n\nfrom chatterbot.adapters import Adapter\nfrom chatterbot.storage import StorageAdapter\nfrom chatterbot.search import IndexedTextSearch\nfrom chatterbot.conversation import Statement\nfrom chatterbot import utils\n\n\nclass LogicAdapter(Adapter):\n    \"\"\"\n    This is an abstract class that represents the interface\n    that all logic adapters should implement.\n\n    :param search_algorithm_name: The name of the search algorithm that should\n        be used to search for close matches to the provided input.\n        Defaults to the value of ``Search.name``.\n\n    :param maximum_similarity_threshold:\n        The maximum amount of similarity between two statement that is required\n        before the search process is halted. The search for a matching statement\n        will continue until a statement with a greater than or equal similarity\n        is found or the search set is exhausted.\n        Defaults to 0.95\n\n    :param response_selection_method:\n          The a response selection method.\n          Defaults to ``get_first_response``\n    :type response_selection_method: collections.abc.Callable\n\n    :param default_response:\n          The default response returned by this logic adapter\n          if there is no other possible response to return.\n    :type default_response: str or list or tuple\n    \"\"\"\n\n    def __init__(self, chatbot, **kwargs):\n        super().__init__(chatbot, **kwargs)\n        from chatterbot.response_selection import get_first_response\n\n        self.search_algorithm_name = kwargs.get(\n            'search_algorithm_name',\n            IndexedTextSearch.name\n        )\n\n        self.search_algorithm = self.chatbot.search_algorithms[\n            self.search_algorithm_name\n        ]\n\n        self.maximum_similarity_threshold = kwargs.get(\n            'maximum_similarity_threshold', 0.95\n        )\n\n        if response_selection_method := kwargs.get('response_selection_method'):\n            if isinstance(response_selection_method, str):\n                # If an import path is provided, import the method\n                response_selection_method = utils.import_module(\n                    response_selection_method\n                )\n                kwargs['response_selection_method'] = response_selection_method\n\n        # By default, select the first available response\n        self.select_response = kwargs.get(\n            'response_selection_method',\n            get_first_response\n        )\n\n        default_responses = kwargs.get('default_response', [])\n\n        # Convert a single string into a list\n        if isinstance(default_responses, str):\n            default_responses = [\n                default_responses\n            ]\n\n        self.default_responses = [\n            Statement(text=default) for default in default_responses\n        ]\n\n    def can_process(self, statement) -> bool:\n        \"\"\"\n        A preliminary check that is called to determine if a\n        logic adapter can process a given statement. By default,\n        this method returns true but it can be overridden in\n        child classes as needed.\n        \"\"\"\n        return True\n\n    def process(self, statement: Statement, additional_response_selection_parameters: dict = None) -> Statement:\n        \"\"\"\n        Override this method and implement your logic for selecting a response to an input statement.\n\n        A confidence value and the selected response statement should be returned.\n        The confidence value represents a rating of how accurate the logic adapter\n        expects the selected response to be. Confidence scores are used to select\n        the best response from multiple logic adapters.\n\n        The confidence value should be a number between 0 and 1 where 0 is the\n        lowest confidence level and 1 is the highest.\n\n        :param statement: An input statement to be processed by the logic adapter.\n\n        :param additional_response_selection_parameters: Parameters to be used when\n            filtering results to choose a response from.\n        \"\"\"\n        raise self.AdapterMethodNotImplementedError()\n\n    def get_default_response(self, input_statement: Statement) -> Statement:\n        \"\"\"\n        This method is called when a logic adapter is unable to generate any\n        other meaningful response.\n        \"\"\"\n        if self.default_responses:\n            response = choice(self.default_responses)\n        else:\n            try:\n                response = self.chatbot.storage.get_random()\n            except StorageAdapter.EmptyDatabaseException:\n                response = input_statement\n\n        self.chatbot.logger.info(\n            'No known response to the input was found. Selecting a random response.'\n        )\n\n        # Set confidence to zero because a random response is selected\n        response.confidence = 0\n\n        return response\n\n    @property\n    def class_name(self) -> str:\n        \"\"\"\n        Return the name of the current logic adapter class.\n        This is typically used for logging and debugging.\n        \"\"\"\n        return str(self.__class__.__name__)\n"
  },
  {
    "path": "chatterbot/logic/mathematical_evaluation.py",
    "content": "from chatterbot.logic import LogicAdapter\nfrom chatterbot.conversation import Statement\nfrom chatterbot import languages\nfrom chatterbot.logic.mcp_tools import MCPToolAdapter\n\n\nclass MathematicalEvaluation(LogicAdapter, MCPToolAdapter):\n    \"\"\"\n    The MathematicalEvaluation logic adapter parses input to determine\n    whether the user is asking a question that requires math to be done.\n    If so, the equation is extracted from the input and returned with\n    the evaluated result.\n\n    For example:\n        User: 'What is three plus five?'\n        Bot: 'Three plus five equals eight'\n\n    :kwargs:\n        * *language* (``object``) --\n          The language is set to ``chatterbot.languages.ENG`` for English by default.\n    \"\"\"\n\n    def __init__(self, chatbot, **kwargs):\n        super().__init__(chatbot, **kwargs)\n\n        self.language = kwargs.get('language', languages.ENG)\n        self.cache = {}\n\n    def can_process(self, statement) -> bool:\n        \"\"\"\n        Determines whether it is appropriate for this\n        adapter to respond to the user input.\n        \"\"\"\n        response = self.process(statement)\n        self.cache[statement.text] = response\n        return response.confidence == 1\n\n    def process(self, statement: Statement, additional_response_selection_parameters: dict = None) -> Statement:\n        \"\"\"\n        Takes a statement string.\n        Returns the equation from the statement with the mathematical terms solved.\n        \"\"\"\n        from mathparse import mathparse\n\n        input_text = statement.text\n\n        # Use the result cached by the process method if it exists\n        if input_text in self.cache:\n            cached_result = self.cache[input_text]\n            self.cache = {}\n            return cached_result\n\n        # Getting the mathematical terms within the input statement\n        expression = mathparse.extract_expression(input_text, language=self.language.ISO_639.upper())\n\n        response = Statement(text=expression)\n\n        try:\n            response.text = '{} = {}'.format(\n                response.text,\n                mathparse.parse(expression, language=self.language.ISO_639.upper())\n            )\n\n            # The confidence is 1 if the expression could be evaluated\n            response.confidence = 1\n        except mathparse.PostfixTokenEvaluationException:\n            response.confidence = 0\n\n        return response\n\n    def get_tool_schema(self):\n        \"\"\"\n        Return the MCP tool schema for mathematical evaluation.\n        \"\"\"\n        return {\n            \"name\": \"calculate\",\n            \"description\": \"Evaluate mathematical expressions and solve equations. Supports basic arithmetic, algebra, and common mathematical functions.\",\n            \"parameters\": {\n                \"type\": \"object\",\n                \"properties\": {\n                    \"expression\": {\n                        \"type\": \"string\",\n                        \"description\": \"The mathematical expression to evaluate (e.g., '2 + 2', 'sqrt(16)', 'three plus five')\"\n                    }\n                },\n                \"required\": [\"expression\"]\n            }\n        }\n\n    def execute_as_tool(self, **kwargs):\n        \"\"\"\n        Execute mathematical evaluation as a tool.\n\n        Args:\n            **kwargs: Must contain 'expression' parameter\n\n        Returns:\n            The evaluation result as a string\n        \"\"\"\n        from mathparse import mathparse\n\n        expression = kwargs.get(\"expression\", \"\")\n\n        if not expression:\n            return \"Error: No expression provided\"\n\n        try:\n            # Extract mathematical expression\n            extracted = mathparse.extract_expression(expression, language=self.language.ISO_639.upper())\n\n            # Evaluate the expression\n            result = mathparse.parse(extracted, language=self.language.ISO_639.upper())\n\n            return f\"{extracted} = {result}\"\n\n        except mathparse.PostfixTokenEvaluationException:\n            return f\"Error: Could not evaluate expression '{expression}'\"\n        except Exception as e:\n            return f\"Error: {str(e)}\"\n"
  },
  {
    "path": "chatterbot/logic/mcp_tools.py",
    "content": "\"\"\"\nMCP (Model Context Protocol) tool adapter for ChatterBot logic adapters.\n\nThis module provides a mixin class that allows logic adapters to be exposed\nas MCP-compatible tools to LLMs. Logic adapters that inherit from MCPToolAdapter\ncan define tool schemas and be invoked by LLM adapters.\n\"\"\"\nfrom typing import Any, Dict\nfrom abc import ABC, abstractmethod\n\n\nclass MCPToolAdapter(ABC):\n    \"\"\"\n    Mixin class for logic adapters that can be used as MCP tools.\n\n    Logic adapters that want to be callable as tools should inherit from this\n    class and implement the get_tool_schema() and execute_as_tool() methods.\n\n    Example:\n        class MathematicalEvaluation(LogicAdapter, MCPToolAdapter):\n            def get_tool_schema(self) -> Dict[str, Any]:\n                return {\n                    \"name\": \"calculate\",\n                    \"description\": \"Evaluate mathematical expressions\",\n                    \"parameters\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                            \"expression\": {\n                                \"type\": \"string\",\n                                \"description\": \"Mathematical expression to evaluate\"\n                            }\n                        },\n                        \"required\": [\"expression\"]\n                    }\n                }\n\n            def execute_as_tool(self, **kwargs) -> str:\n                expression = kwargs.get(\"expression\")\n                # ... evaluation logic\n                return result\n    \"\"\"\n\n    @abstractmethod\n    def get_tool_schema(self) -> Dict[str, Any]:\n        \"\"\"\n        Return the tool schema for this logic adapter.\n\n        The schema should follow the OpenAI/MCP function calling format:\n        {\n            \"name\": \"tool_name\",\n            \"description\": \"Tool description\",\n            \"parameters\": {\n                \"type\": \"object\",\n                \"properties\": {\n                    \"param_name\": {\n                        \"type\": \"string|number|boolean|array|object\",\n                        \"description\": \"Parameter description\"\n                    }\n                },\n                \"required\": [\"param_name\"]\n            }\n        }\n\n        Returns:\n            Dict containing the tool schema\n        \"\"\"\n        raise NotImplementedError(\n            \"Logic adapters using MCPToolAdapter must implement get_tool_schema()\"\n        )\n\n    @abstractmethod\n    def execute_as_tool(self, **kwargs) -> Any:\n        \"\"\"\n        Execute this logic adapter as a tool with the given parameters.\n\n        This method is called when an LLM requests to use this adapter as a tool.\n        It should extract the necessary parameters from kwargs and execute the\n        logic adapter's functionality in a tool-calling context.\n\n        Args:\n            **kwargs: Tool parameters as defined in the tool schema\n\n        Returns:\n            Tool execution result (will be converted to string if needed)\n        \"\"\"\n        raise NotImplementedError(\n            \"Logic adapters using MCPToolAdapter must implement execute_as_tool()\"\n        )\n\n    def get_tool_name(self) -> str:\n        \"\"\"\n        Get the name of this tool.\n\n        Returns:\n            The tool name from the schema\n        \"\"\"\n        schema = self.get_tool_schema()\n        return schema.get(\"name\", self.__class__.__name__)\n\n    def validate_tool_parameters(self, **kwargs) -> bool:\n        \"\"\"\n        Validate that the provided parameters match the tool schema.\n\n        Args:\n            **kwargs: Parameters to validate\n\n        Returns:\n            True if parameters are valid, False otherwise\n        \"\"\"\n        schema = self.get_tool_schema()\n        parameters = schema.get(\"parameters\", {})\n        required = parameters.get(\"required\", [])\n        properties = parameters.get(\"properties\", {})\n\n        # Check required parameters\n        for param in required:\n            if param not in kwargs:\n                return False\n\n        # Check parameter types (basic validation)\n        for param_name, param_value in kwargs.items():\n            if param_name not in properties:\n                continue\n\n            expected_type = properties[param_name].get(\"type\")\n            if expected_type == \"string\" and not isinstance(param_value, str):\n                return False\n            elif expected_type == \"number\" and not isinstance(param_value, (int, float)):\n                return False\n            elif expected_type == \"boolean\" and not isinstance(param_value, bool):\n                return False\n            elif expected_type == \"array\" and not isinstance(param_value, list):\n                return False\n            elif expected_type == \"object\" and not isinstance(param_value, dict):\n                return False\n\n        return True\n\n\ndef is_tool_adapter(adapter) -> bool:\n    \"\"\"\n    Check if a logic adapter instance supports MCP tool functionality.\n\n    Args:\n        adapter: Logic adapter instance to check\n\n    Returns:\n        True if the adapter has MCPToolAdapter capabilities\n    \"\"\"\n    return (\n        hasattr(adapter, 'get_tool_schema') and\n        callable(adapter.get_tool_schema) and\n        hasattr(adapter, 'execute_as_tool') and\n        callable(adapter.execute_as_tool)\n    )\n\n\ndef convert_to_openai_tool_format(schema: Dict[str, Any]) -> Dict[str, Any]:\n    \"\"\"\n    Convert MCP tool schema to OpenAI function calling format.\n\n    OpenAI expects:\n    {\n        \"type\": \"function\",\n        \"function\": {\n            \"name\": \"...\",\n            \"description\": \"...\",\n            \"parameters\": {...}\n        }\n    }\n\n    Args:\n        schema: MCP tool schema\n\n    Returns:\n        OpenAI-formatted tool definition\n    \"\"\"\n    return {\n        \"type\": \"function\",\n        \"function\": {\n            \"name\": schema.get(\"name\"),\n            \"description\": schema.get(\"description\", \"\"),\n            \"parameters\": schema.get(\"parameters\", {})\n        }\n    }\n\n\ndef convert_to_ollama_tool_format(schema: Dict[str, Any]) -> Dict[str, Any]:\n    \"\"\"\n    Convert MCP tool schema to Ollama function calling format.\n\n    Ollama uses a similar format to OpenAI:\n    {\n        \"type\": \"function\",\n        \"function\": {\n            \"name\": \"...\",\n            \"description\": \"...\",\n            \"parameters\": {...}\n        }\n    }\n\n    Args:\n        schema: MCP tool schema\n\n    Returns:\n        Ollama-formatted tool definition\n    \"\"\"\n    # Ollama format is identical to OpenAI for now\n    return convert_to_openai_tool_format(schema)\n"
  },
  {
    "path": "chatterbot/logic/specific_response.py",
    "content": "from chatterbot.logic import LogicAdapter\nfrom chatterbot.conversation import Statement\nfrom chatterbot import languages\nfrom chatterbot.utils import get_model_for_language\nimport spacy\n\n\nclass SpecificResponseAdapter(LogicAdapter):\n    \"\"\"\n    Return a specific response to a specific input.\n\n    :kwargs:\n        * *input_text* (``str``) --\n          The input text that triggers this logic adapter.\n        * *output_text* (``str`` or ``function``) --\n          The output text returned by this logic adapter.\n          If a function is provided, it should return a string.\n    \"\"\"\n\n    def __init__(self, chatbot, **kwargs):\n        super().__init__(chatbot, **kwargs)\n\n        try:\n            self.input_text = kwargs['input_text']\n        except KeyError:\n            raise chatbot.ChatBotException(\n                'The SpecificResponseAdapter requires an input_text parameter.'\n            )\n\n        try:\n            self._output_text = kwargs['output_text']\n        except KeyError:\n            raise chatbot.ChatBotException(\n                'The SpecificResponseAdapter requires an output_text parameter.'\n            )\n\n        self.matcher = None\n\n        if MatcherClass := kwargs.get('matcher'):\n            language = kwargs.get('language', languages.ENG)\n\n            self.nlp = self._initialize_nlp(language)\n\n            self.matcher = MatcherClass(self.nlp.vocab)\n\n            self.matcher.add('SpecificResponse', [self.input_text])\n\n    def _initialize_nlp(self, language):\n        model = get_model_for_language(language)\n\n        return spacy.load(model)\n\n    def can_process(self, statement) -> bool:\n        if self.matcher:\n            doc = self.nlp(statement.text)\n            matches = self.matcher(doc)\n\n            if matches:\n                return True\n        elif statement.text == self.input_text:\n            return True\n\n        return False\n\n    def process(self, statement: Statement, additional_response_selection_parameters: dict = None) -> Statement:\n\n        if callable(self._output_text):\n            response_statement = Statement(text=self._output_text())\n        else:\n            response_statement = Statement(text=self._output_text)\n\n        if self.matcher:\n            doc = self.nlp(statement.text)\n            matches = self.matcher(doc)\n\n            if matches:\n                response_statement.confidence = 1\n            else:\n                response_statement.confidence = 0\n\n        elif statement.text == self.input_text:\n            response_statement.confidence = 1\n        else:\n            response_statement.confidence = 0\n\n        return response_statement\n"
  },
  {
    "path": "chatterbot/logic/time_adapter.py",
    "content": "from datetime import datetime\nfrom chatterbot import languages\nfrom chatterbot.logic import LogicAdapter\nfrom chatterbot.conversation import Statement\nfrom chatterbot.utils import get_model_for_language\nfrom chatterbot.logic.mcp_tools import MCPToolAdapter\nimport spacy\n\n\nclass TimeLogicAdapter(LogicAdapter, MCPToolAdapter):\n    \"\"\"\n    The TimeLogicAdapter returns the current time.\n\n    :kwargs:\n        * *positive* (``list``) --\n          The time-related questions used to identify time questions about the current time.\n          Defaults to a list of English sentences.\n        * *language* (``str``) --\n          The language for the spacy model. Defaults to English.\n    \"\"\"\n\n    def __init__(self, chatbot, **kwargs):\n        super().__init__(chatbot, **kwargs)\n\n        # TODO / FUTURE: Switch `positive` to `patterns` for more accurate naming\n        phrases = kwargs.get('positive', [\n            'What time is it?',\n            'Hey, what time is it?',\n            'Do you have the time?',\n            'Do you know the time?',\n            'Do you know what time it is?',\n            'What is the time?',\n            'What time is it now?',\n            'Can you tell me the time?',\n            'Could you tell me the time?',\n            'What is the current time?',\n        ])\n\n        language = kwargs.get('language', languages.ENG)\n\n        model = get_model_for_language(language)\n\n        self.nlp = spacy.load(model)\n\n        # Set up rules for spacy's rule-based matching\n        # https://spacy.io/usage/rule-based-matching\n\n        self.matcher = spacy.matcher.PhraseMatcher(self.nlp.vocab)\n\n        patterns = [self.nlp.make_doc(text) for text in phrases]\n\n        # Add the patterns to the matcher\n        self.matcher.add('TimeQuestionList', patterns)\n\n    def process(self, statement: Statement, additional_response_selection_parameters: dict = None) -> Statement:\n        now = datetime.now()\n\n        # Check if the input statement contains a time-related question\n        doc = self.nlp(statement.text)\n\n        matches = self.matcher(doc)\n\n        self.chatbot.logger.info('TimeLogicAdapter detected {} matches'.format(len(matches)))\n\n        confidence = 1 if matches else 0\n        response = Statement(text='The current time is ' + now.strftime('%I:%M %p'))\n\n        response.confidence = confidence\n        return response\n\n    def get_tool_schema(self):\n        \"\"\"\n        Return the MCP tool schema for getting current time.\n        \"\"\"\n        return {\n            \"name\": \"get_current_time\",\n            \"description\": \"Get the current date and time. Returns formatted time string.\",\n            \"parameters\": {\n                \"type\": \"object\",\n                \"properties\": {},\n                \"required\": []\n            }\n        }\n\n    def execute_as_tool(self, **kwargs):\n        \"\"\"\n        Execute time query as a tool.\n\n        Returns:\n            Current time as formatted string\n        \"\"\"\n        now = datetime.now()\n        return f\"The current time is {now.strftime('%I:%M %p')} on {now.strftime('%A, %B %d, %Y')}\"\n"
  },
  {
    "path": "chatterbot/logic/unit_conversion.py",
    "content": "from chatterbot.logic import LogicAdapter\nfrom chatterbot.conversation import Statement\nfrom chatterbot.exceptions import OptionalDependencyImportError\nfrom chatterbot import languages\nfrom chatterbot import parsing\nfrom chatterbot.logic.mcp_tools import MCPToolAdapter\nfrom mathparse import mathparse\nimport re\n\n\nclass UnitConversion(LogicAdapter, MCPToolAdapter):\n    \"\"\"\n    The UnitConversion logic adapter parse inputs to convert values\n    between several metric units.\n\n    For example:\n        User: 'How many meters are in one kilometer?'\n        Bot: '1000.0'\n\n    :kwargs:\n        * *language* (``object``) --\n        The language is set to ``chatterbot.languages.ENG`` for English by default.\n    \"\"\"\n\n    def __init__(self, chatbot, **kwargs):\n        super().__init__(chatbot, **kwargs)\n        try:\n            from pint import UnitRegistry\n        except ImportError:\n            message = (\n                'Unable to import \"pint\".\\n'\n                'Please install \"pint\" before using the UnitConversion logic adapter:\\n'\n                'pip install pint'\n            )\n            raise OptionalDependencyImportError(message)\n\n        self.language = kwargs.get('language', languages.ENG)\n        self.cache = {}\n        self.patterns = [\n            (\n                re.compile(r'''\n                   (([Hh]ow\\s+many)\\s+\n                   (?P<target>\\S+)\\s+ # meter, celsius, hours\n                   ((are)*\\s*in)\\s+\n                   (?P<number>([+-]?\\d+(?:\\.\\d+)?)|(a|an)|(%s[-\\s]?)+)\\s+\n                   (?P<from>\\S+)\\s*) # meter, celsius, hours\n                   ''' % (parsing.numbers),\n                    (re.VERBOSE | re.IGNORECASE)\n                ),\n                lambda m: self.handle_matches(m)\n            ),\n            (\n                re.compile(r'''\n                   ((?P<number>([+-]?\\d+(?:\\.\\d+)?)|(%s[-\\s]?)+)\\s+\n                   (?P<from>\\S+)\\s+ # meter, celsius, hours\n                   (to)\\s+\n                   (?P<target>\\S+)\\s*) # meter, celsius, hours\n                   ''' % (parsing.numbers),\n                    (re.VERBOSE | re.IGNORECASE)\n                ),\n                lambda m: self.handle_matches(m)\n            ),\n            (\n                re.compile(r'''\n                   ((?P<number>([+-]?\\d+(?:\\.\\d+)?)|(a|an)|(%s[-\\s]?)+)\\s+\n                   (?P<from>\\S+)\\s+ # meter, celsius, hours\n                   (is|are)\\s+\n                   (how\\s+many)*\\s+\n                   (?P<target>\\S+)\\s*) # meter, celsius, hours\n                   ''' % (parsing.numbers),\n                    (re.VERBOSE | re.IGNORECASE)\n                ),\n                lambda m: self.handle_matches(m)\n            )\n        ]\n        self.unit_registry = UnitRegistry()\n\n    def get_unit(self, unit_variations):\n        \"\"\"\n        Get the first match unit metric object supported by pint library\n        given a variation of unit metric names (Ex:['HOUR', 'hour']).\n\n        :param unit_variations: A list of strings with names of units\n        :type unit_variations: str\n        \"\"\"\n        for unit in unit_variations:\n            try:\n                return getattr(self.unit_registry, unit)\n            except AttributeError:\n                continue\n        return None\n\n    def get_valid_units(self, from_unit, target_unit):\n        \"\"\"\n        Returns the first match `pint.unit.Unit` object for from_unit and\n        target_unit strings from a possible variation of metric unit names\n        supported by pint library.\n\n        :param from_unit: source metric unit\n        :type from_unit: str\n\n        :param from_unit: target metric unit\n        :type from_unit: str\n        \"\"\"\n        from_unit_variations = [from_unit.lower(), from_unit.upper()]\n        target_unit_variations = [target_unit.lower(), target_unit.upper()]\n        from_unit = self.get_unit(from_unit_variations)\n        target_unit = self.get_unit(target_unit_variations)\n        return from_unit, target_unit\n\n    def handle_matches(self, match):\n        \"\"\"\n        Returns a response statement from a matched input statement.\n\n        :param match: It is a valid matched pattern from the input statement\n        :type: `_sre.SRE_Match`\n        \"\"\"\n        response = Statement(text='')\n\n        from_parsed = match.group(\"from\")\n        target_parsed = match.group(\"target\")\n        n_statement = match.group(\"number\")\n\n        if n_statement == 'a' or n_statement == 'an':\n            n_statement = '1.0'\n\n        n = mathparse.parse(n_statement, self.language.ISO_639.upper())\n\n        from_parsed, target_parsed = self.get_valid_units(from_parsed, target_parsed)\n\n        if from_parsed is None or target_parsed is None:\n            response.confidence = 0.0\n        else:\n            from_value = self.unit_registry.Quantity(float(n), from_parsed)\n            target_value = from_value.to(target_parsed)\n            response.confidence = 1.0\n            response.text = str(target_value.magnitude)\n\n        return response\n\n    def can_process(self, statement) -> bool:\n        response = self.process(statement)\n        self.cache[statement.text] = response\n        return response.confidence == 1.0\n\n    def process(self, statement: Statement, additional_response_selection_parameters: dict = None) -> Statement:\n        response = Statement(text='')\n        input_text = statement.text\n        try:\n            # Use the result cached by the process method if it exists\n            if input_text in self.cache:\n                response = self.cache[input_text]\n                self.cache = {}\n                return response\n\n            for pattern, func in self.patterns:\n                p = pattern.match(input_text)\n                if p is not None:\n                    response = func(p)\n                    if response.confidence == 1.0:\n                        break\n        except Exception as e:\n            self.chatbot.logger.warning('Error during UnitConversion: {}'.format(str(e)))\n            response.confidence = 0.0\n\n        return response\n\n    def get_tool_schema(self):\n        \"\"\"\n        Return the MCP tool schema for unit conversion.\n        \"\"\"\n        return {\n            \"name\": \"convert_units\",\n            \"description\": \"Convert values between different units of measurement. Supports distance, weight, temperature, time, and other common unit conversions.\",\n            \"parameters\": {\n                \"type\": \"object\",\n                \"properties\": {\n                    \"query\": {\n                        \"type\": \"string\",\n                        \"description\": \"Unit conversion query in natural language (e.g., 'How many meters are in 5 kilometers?', '100 fahrenheit to celsius')\"\n                    }\n                },\n                \"required\": [\"query\"]\n            }\n        }\n\n    def execute_as_tool(self, **kwargs):\n        \"\"\"\n        Execute unit conversion as a tool.\n\n        Args:\n            **kwargs: Must contain 'query' parameter\n\n        Returns:\n            The conversion result as a string\n        \"\"\"\n        query = kwargs.get(\"query\", \"\")\n\n        if not query:\n            return \"Error: No conversion query provided\"\n\n        try:\n            # Create a statement and process it\n            input_statement = Statement(text=query)\n            response = self.process(input_statement)\n\n            if response.confidence == 1.0:\n                return response.text\n            else:\n                return f\"Error: Could not parse unit conversion from '{query}'. Try formats like 'X units to Y units' or 'How many Y in X units?'\"\n\n        except Exception as e:\n            return f\"Error: {str(e)}\"\n"
  },
  {
    "path": "chatterbot/parsing.py",
    "content": "import re\nfrom datetime import timedelta, datetime\nimport calendar\n\n# Variations of dates that the parser can capture\nyear_variations = ['year', 'years', 'yrs']\nday_variations = ['days', 'day']\nminute_variations = ['minute', 'minutes', 'mins']\nhour_variations = ['hrs', 'hours', 'hour']\nweek_variations = ['weeks', 'week', 'wks']\nmonth_variations = ['month', 'months']\n\n# Variables used for RegEx Matching\nday_names = 'monday|tuesday|wednesday|thursday|friday|saturday|sunday'\nmonth_names_long = (\n    'january|february|march|april|may|june|july|august|september|october|november|december'\n)\nmonth_names = month_names_long + '|jan|feb|mar|apr|may|jun|jul|aug|sep|oct|nov|dec'\nday_nearest_names = 'today|yesterday|tomorrow|tonight|tonite'\nnumbers = (\n    r'(^a(?=\\s)|one|two|three|four|five|six|seven|eight|nine|ten|'\n    r'eleven|twelve|thirteen|fourteen|fifteen|sixteen|seventeen|'\n    r'eighteen|nineteen|twenty|thirty|forty|fifty|sixty|seventy|'\n    r'eighty|ninety|hundred|thousand)'\n)\nre_dmy = '(' + '|'.join(day_variations + minute_variations + year_variations + week_variations + month_variations) + ')'\nre_duration = r'(before|after|earlier|later|ago|from\\snow)'\nre_year = r'(19|20)\\d{2}|^(19|20)\\d{2}'\nre_timeframe = r'this|coming|next|following|previous|last|end\\sof\\sthe'\nre_ordinal = r'st|nd|rd|th|first|second|third|fourth|fourth|' + re_timeframe\nre_time = r'(?P<hour>\\d{1,2})(?=\\s?(\\:\\d|(a|p)m))(\\:(?P<minute>\\d{1,2}))?(\\s?(?P<convention>(am|pm)))?'\nre_separator = r'of|at|on'\n\nNUMBERS = {\n    'zero': 0,\n    'one': 1,\n    'two': 2,\n    'three': 3,\n    'four': 4,\n    'five': 5,\n    'six': 6,\n    'seven': 7,\n    'eight': 8,\n    'nine': 9,\n    'ten': 10,\n    'eleven': 11,\n    'twelve': 12,\n    'thirteen': 13,\n    'fourteen': 14,\n    'fifteen': 15,\n    'sixteen': 16,\n    'seventeen': 17,\n    'eighteen': 18,\n    'nineteen': 19,\n    'twenty': 20,\n    'thirty': 30,\n    'forty': 40,\n    'fifty': 50,\n    'sixty': 60,\n    'seventy': 70,\n    'eighty': 80,\n    'ninety': 90,\n    'hundred': 100,\n    'thousand': 1000,\n    'million': 1000000,\n    'billion': 1000000000,\n    'trillion': 1000000000000,\n}\n\n\n# Mapping of Month name and Value\nHASHMONTHS = {\n    'january': 1,\n    'jan': 1,\n    'february': 2,\n    'feb': 2,\n    'march': 3,\n    'mar': 3,\n    'april': 4,\n    'apr': 4,\n    'may': 5,\n    'june': 6,\n    'jun': 6,\n    'july': 7,\n    'jul': 7,\n    'august': 8,\n    'aug': 8,\n    'september': 9,\n    'sep': 9,\n    'october': 10,\n    'oct': 10,\n    'november': 11,\n    'nov': 11,\n    'december': 12,\n    'dec': 12\n}\n\n# Days to number mapping\nHASHWEEKDAYS = {\n    'monday': 0,\n    'mon': 0,\n    'tuesday': 1,\n    'tue': 1,\n    'wednesday': 2,\n    'wed': 2,\n    'thursday': 3,\n    'thu': 3,\n    'friday': 4,\n    'fri': 4,\n    'saturday': 5,\n    'sat': 5,\n    'sunday': 6,\n    'sun': 6\n}\n\n# Ordinal to number\nHASHORDINALS = {\n    'zeroth': 0,\n    'first': 1,\n    'second': 2,\n    'third': 3,\n    'fourth': 4,\n    'forth': 4,\n    'fifth': 5,\n    'sixth': 6,\n    'seventh': 7,\n    'eighth': 8,\n    'ninth': 9,\n    'tenth': 10,\n    'eleventh': 11,\n    'twelfth': 12,\n    'thirteenth': 13,\n    'fourteenth': 14,\n    'fifteenth': 15,\n    'sixteenth': 16,\n    'seventeenth': 17,\n    'eighteenth': 18,\n    'nineteenth': 19,\n    'twentieth': 20,\n    'last': -1\n}\n\n# A list tuple of regular expressions / parser fn to match\n# Start with the widest match and narrow it down because the order of the match in this list matters\nregex = [\n    (\n        re.compile(\n            r'''\n            (\n                ((?P<dow>%s)[,\\s]\\s*)? #Matches Monday, 12 Jan 2012, 12 Jan 2012 etc\n                (?P<day>\\d{1,2}) # Matches a digit\n                (%s)?\n                [-\\s] # One or more space\n                (?P<month>%s) # Matches any month name\n                [-\\s] # Space\n                (?P<year>%s) # Year\n                ((\\s|,\\s|\\s(%s))?\\s*(%s))?\n            )\n            ''' % (day_names, re_ordinal, month_names, re_year, re_separator, re_time),\n            (re.VERBOSE | re.IGNORECASE)\n        ),\n        lambda m, base_date: datetime(\n            int(m.group('year') if m.group('year') else base_date.year),\n            HASHMONTHS[m.group('month').strip().lower()],\n            int(m.group('day') if m.group('day') else 1),\n        ) + timedelta(**convert_time_to_hour_minute(\n            m.group('hour'),\n            m.group('minute'),\n            m.group('convention')\n        ))\n    ),\n    (\n        re.compile(\n            r'''\n            (\n                ((?P<dow>%s)[,\\s][-\\s]*)? #Matches Monday, Jan 12 2012, Jan 12 2012 etc\n                (?P<month>%s) # Matches any month name\n                [-\\s] # Space\n                ((?P<day>\\d{1,2})) # Matches a digit\n                (%s)?\n                ([-\\s](?P<year>%s))? # Year\n                ((\\s|,\\s|\\s(%s))?\\s*(%s))?\n            )\n            ''' % (day_names, month_names, re_ordinal, re_year, re_separator, re_time),\n            (re.VERBOSE | re.IGNORECASE)\n        ),\n        lambda m, base_date: datetime(\n            int(m.group('year') if m.group('year') else base_date.year),\n            HASHMONTHS[m.group('month').strip().lower()],\n            int(m.group('day') if m.group('day') else 1)\n        ) + timedelta(**convert_time_to_hour_minute(\n            m.group('hour'),\n            m.group('minute'),\n            m.group('convention')\n        ))\n    ),\n    (\n        re.compile(\n            r'''\n            (\n                (?P<month>%s) # Matches any month name\n                [-\\s] # One or more space\n                (?P<day>\\d{1,2}) # Matches a digit\n                (%s)?\n                [-\\s]\\s*?\n                (?P<year>%s) # Year\n                ((\\s|,\\s|\\s(%s))?\\s*(%s))?\n            )\n            ''' % (month_names, re_ordinal, re_year, re_separator, re_time),\n            (re.VERBOSE | re.IGNORECASE)\n        ),\n        lambda m, base_date: datetime(\n            int(m.group('year') if m.group('year') else base_date.year),\n            HASHMONTHS[m.group('month').strip().lower()],\n            int(m.group('day') if m.group('day') else 1),\n        ) + timedelta(**convert_time_to_hour_minute(\n            m.group('hour'),\n            m.group('minute'),\n            m.group('convention')\n        ))\n    ),\n    (\n        re.compile(\n            r'''\n            (\n                ((?P<number>\\d+|(%s[-\\s]?)+)\\s)? # Matches any number or string 25 or twenty five\n                (?P<unit>%s)s?\\s # Matches days, months, years, weeks, minutes\n                (?P<duration>%s) # before, after, earlier, later, ago, from now\n                (\\s*(?P<base_time>(%s)))?\n                ((\\s|,\\s|\\s(%s))?\\s*(%s))?\n            )\n            ''' % (numbers, re_dmy, re_duration, day_nearest_names, re_separator, re_time),\n            (re.VERBOSE | re.IGNORECASE)\n        ),\n        lambda m, base_date: date_from_duration(\n            base_date,\n            m.group('number'),\n            m.group('unit').lower(),\n            m.group('duration').lower(),\n            m.group('base_time')\n        ) + timedelta(**convert_time_to_hour_minute(\n            m.group('hour'),\n            m.group('minute'),\n            m.group('convention')\n        ))\n    ),\n    (\n        re.compile(\n            r'''\n            (\n                (?P<ordinal>%s) # First quarter of 2014\n                \\s+\n                quarter\\sof\n                \\s+\n                (?P<year>%s)\n            )\n            ''' % (re_ordinal, re_year),\n            (re.VERBOSE | re.IGNORECASE)\n        ),\n        lambda m, base_date: date_from_quarter(\n            base_date,\n            HASHORDINALS[m.group('ordinal').lower()],\n            int(m.group('year') if m.group('year') else base_date.year)\n        )\n    ),\n    (\n        re.compile(\n            r'''\n            (\n                (?P<ordinal_value>\\d+)\n                (?P<ordinal>%s) # 1st January 2012\n                ((\\s|,\\s|\\s(%s))?\\s*)?\n                (?P<month>%s)\n                ([,\\s]\\s*(?P<year>%s))?\n            )\n            ''' % (re_ordinal, re_separator, month_names, re_year),\n            (re.VERBOSE | re.IGNORECASE)\n        ),\n        lambda m, base_date: datetime(\n            int(m.group('year') if m.group('year') else base_date.year),\n            int(HASHMONTHS[m.group('month').lower()] if m.group('month') else 1),\n            int(m.group('ordinal_value') if m.group('ordinal_value') else 1),\n        )\n    ),\n    (\n        re.compile(\n            r'''\n            (\n                (?P<month>%s)\n                \\s+\n                (?P<ordinal_value>\\d+)\n                (?P<ordinal>%s) # January 1st 2012\n                ([,\\s]\\s*(?P<year>%s))?\n            )\n            ''' % (month_names, re_ordinal, re_year),\n            (re.VERBOSE | re.IGNORECASE)\n        ),\n        lambda m, base_date: datetime(\n            int(m.group('year') if m.group('year') else base_date.year),\n            int(HASHMONTHS[m.group('month').lower()] if m.group('month') else 1),\n            int(m.group('ordinal_value') if m.group('ordinal_value') else 1),\n        )\n    ),\n    (\n        re.compile(\n            r'''\n            (?P<time>%s) # this, next, following, previous, last\n            \\s+\n            ((?P<number>\\d+|(%s[-\\s]?)+)\\s)?\n            (?P<dmy>%s) # year, day, week, month, night, minute, min\n            ((\\s|,\\s|\\s(%s))?\\s*(%s))?\n            ''' % (re_timeframe, numbers, re_dmy, re_separator, re_time),\n            (re.VERBOSE | re.IGNORECASE),\n        ),\n        lambda m, base_date: date_from_relative_week_year(\n            base_date,\n            m.group('time').lower(),\n            m.group('dmy').lower(),\n            m.group('number')\n        ) + timedelta(**convert_time_to_hour_minute(\n            m.group('hour'),\n            m.group('minute'),\n            m.group('convention')\n        ))\n    ),\n    (\n        re.compile(\n            r'''\n            (?P<time>%s) # this, next, following, previous, last\n            \\s+\n            (?P<dow>%s) # mon - fri\n            ((\\s|,\\s|\\s(%s))?\\s*(%s))?\n            ''' % (re_timeframe, day_names, re_separator, re_time),\n            (re.VERBOSE | re.IGNORECASE),\n        ),\n        lambda m, base_date: date_from_relative_day(\n            base_date,\n            m.group('time').lower(),\n            m.group('dow')\n        ) + timedelta(**convert_time_to_hour_minute(\n            m.group('hour'),\n            m.group('minute'),\n            m.group('convention')\n        ))\n    ),\n    (\n        re.compile(\n            r'''\n            (\n                (?P<day>\\d{1,2}) # Day, Month\n                (%s)\n                [-\\s] # One or more space\n                (?P<month>%s)\n            )\n            ''' % (re_ordinal, month_names),\n            (re.VERBOSE | re.IGNORECASE)\n        ),\n        lambda m, base_date: datetime(\n            base_date.year,\n            HASHMONTHS[m.group('month').strip().lower()],\n            int(m.group('day') if m.group('day') else 1)\n        )\n    ),\n    (\n        re.compile(\n            r'''\n            (\n                (?P<month>%s) # Month, day\n                [-\\s] # One or more space\n                ((?P<day>\\d{1,2})\\b) # Matches a digit January 12\n                (%s)?\n            )\n            ''' % (month_names, re_ordinal),\n            (re.VERBOSE | re.IGNORECASE)\n        ),\n        lambda m, base_date: datetime(\n            base_date.year,\n            HASHMONTHS[m.group('month').strip().lower()],\n            int(m.group('day') if m.group('day') else 1)\n        )\n    ),\n    (\n        re.compile(\n            r'''\n            (\n                (?P<month>%s) # Month, year\n                [-\\s] # One or more space\n                ((?P<year>\\d{1,4})\\b) # Matches a digit January 12\n            )\n            ''' % (month_names),\n            (re.VERBOSE | re.IGNORECASE)\n        ),\n        lambda m, base_date: datetime(\n            int(m.group('year')),\n            HASHMONTHS[m.group('month').strip().lower()],\n            1\n        )\n    ),\n    (\n        re.compile(\n            r'''\n            (\n                (?P<month>\\d{1,2}) # MM/DD or MM/DD/YYYY\n                /\n                ((?P<day>\\d{1,2}))\n                (/(?P<year>%s))?\n            )\n            ''' % (re_year),\n            (re.VERBOSE | re.IGNORECASE)\n        ),\n        lambda m, base_date: datetime(\n            int(m.group('year') if m.group('year') else base_date.year),\n            int(m.group('month').strip()),\n            int(m.group('day'))\n        )\n    ),\n    (\n        re.compile(\n            r'''\n            (?P<adverb>%s) # today, yesterday, tomorrow, tonight\n            ((\\s|,\\s|\\s(%s))?\\s*(%s))?\n            ''' % (day_nearest_names, re_separator, re_time),\n            (re.VERBOSE | re.IGNORECASE)\n        ),\n        lambda m, base_date: date_from_adverb(\n            base_date,\n            m.group('adverb')\n        ) + timedelta(**convert_time_to_hour_minute(\n            m.group('hour'),\n            m.group('minute'),\n            m.group('convention')\n        ))\n    ),\n    (\n        re.compile(\n            r'''\n            (?P<named_day>%s) # Mon - Sun\n            ''' % (day_names),\n            (re.VERBOSE | re.IGNORECASE)\n        ),\n        lambda m, base_date: this_week_day(\n            base_date,\n            HASHWEEKDAYS[m.group('named_day').lower()]\n        )\n    ),\n    (\n        re.compile(\n            r'''\n            (?P<year>%s) # Year\n            ''' % (re_year),\n            (re.VERBOSE | re.IGNORECASE)\n        ),\n        lambda m, base_date: datetime(int(m.group('year')), 1, 1)\n    ),\n    (\n        re.compile(\n            r'''\n            (?P<month>%s) # Month\n            ''' % (month_names_long),\n            (re.VERBOSE | re.IGNORECASE)\n        ),\n        lambda m, base_date: datetime(\n            base_date.year,\n            HASHMONTHS[m.group('month').lower()],\n            1\n        )\n    ),\n    (\n        re.compile(\n            r'''\n            (%s) # Matches time 12:00 am or 12:00 pm\n            ''' % (re_time),\n            (re.VERBOSE | re.IGNORECASE),\n        ),\n        lambda m, base_date: datetime(\n            base_date.year,\n            base_date.month,\n            base_date.day\n        ) + timedelta(**convert_time_to_hour_minute(\n            m.group('hour'),\n            m.group('minute'),\n            m.group('convention')\n        ))\n    ),\n    (\n        re.compile(\n            r'''\n            (\n                (?P<hour>\\d+) # Matches 12 hours, 2 hrs\n                \\s+\n                (%s)\n            )\n            ''' % ('|'.join(hour_variations)),\n            (re.VERBOSE | re.IGNORECASE),\n        ),\n        lambda m, base_date: datetime(\n            base_date.year,\n            base_date.month,\n            base_date.day,\n            int(m.group('hour'))\n        )\n    )\n]\n\n\ndef convert_string_to_number(value: str) -> int:\n    \"\"\"\n    Convert strings to numbers\n    \"\"\"\n    if value is None:\n        return 1\n    if isinstance(value, int):\n        return value\n    if value.isdigit():\n        return int(value)\n    num_list = map(lambda s: NUMBERS[s], re.findall(numbers + '+', value.lower()))\n    return sum(num_list)\n\n\ndef convert_time_to_hour_minute(hour: str, minute: str, convention: str) -> dict:\n    \"\"\"\n    Convert time to hour, minute\n    \"\"\"\n    if hour is None:\n        hour = 0\n    if minute is None:\n        minute = 0\n    if convention is None:\n        convention = 'am'\n\n    hour = int(hour)\n    minute = int(minute)\n\n    if convention.lower() == 'pm':\n        # Handle 12 PM (noon) - it stays as 12\n        # Handle 1-11 PM - add 12\n        if hour != 12:\n            hour += 12\n    else:\n        # Handle 12 AM (midnight) - convert to 0\n        if hour == 12:\n            hour = 0\n\n    return {'hours': hour, 'minutes': minute}\n\n\ndef date_from_quarter(base_date: datetime, ordinal: int, year: int) -> list[datetime]:\n    \"\"\"\n    Extract date from quarter of a year\n    \"\"\"\n    interval = 3\n    month_start = interval * (ordinal - 1)\n    if month_start < 0:\n        month_start = 9\n    month_end = month_start + interval\n    if month_start == 0:\n        month_start = 1\n    return [\n        datetime(year, month_start, 1),\n        datetime(year, month_end, calendar.monthrange(year, month_end)[1])\n    ]\n\n\ndef date_from_relative_day(base_date: datetime, time: str, dow: str) -> datetime:\n    \"\"\"\n    Converts relative day to time\n    Ex: this tuesday, last tuesday\n    \"\"\"\n    # Reset date to start of the day\n    base_date = datetime(base_date.year, base_date.month, base_date.day)\n    time = time.lower()\n    dow = dow.lower()\n    if time == 'this' or time == 'coming':\n        # Else day of week\n        num = HASHWEEKDAYS[dow]\n        return this_week_day(base_date, num)\n    elif time == 'last' or time == 'previous':\n        # Else day of week\n        num = HASHWEEKDAYS[dow]\n        return previous_week_day(base_date, num)\n    elif time == 'next' or time == 'following':\n        # Else day of week\n        num = HASHWEEKDAYS[dow]\n        return next_week_day(base_date, num)\n\n\ndef date_from_relative_week_year(base_date: datetime, time: str, dow: str, ordinal: int = 1) -> datetime:\n    \"\"\"\n    Converts relative day to time\n    Eg. this tuesday, last tuesday\n    \"\"\"\n    # If there is an ordinal (next 3 weeks) => return a start and end range\n    # Reset date to start of the day\n    relative_date = datetime(base_date.year, base_date.month, base_date.day)\n    ord = convert_string_to_number(ordinal)\n    if dow in year_variations:\n        if time == 'this' or time == 'coming':\n            return datetime(relative_date.year, 1, 1)\n        elif time == 'last' or time == 'previous':\n            return datetime(relative_date.year - 1, relative_date.month, 1)\n        elif time == 'next' or time == 'following':\n            return relative_date + timedelta(ord * 365)\n        elif time == 'end of the':\n            return datetime(relative_date.year, 12, 31)\n    elif dow in month_variations:\n        if time == 'this':\n            return datetime(relative_date.year, relative_date.month, relative_date.day)\n        elif time == 'last' or time == 'previous':\n            return datetime(relative_date.year, relative_date.month - 1, relative_date.day)\n        elif time == 'next' or time == 'following':\n            if relative_date.month + ord >= 12:\n                month = relative_date.month - 1 + ord\n                year = relative_date.year + month // 12\n                month = month % 12 + 1\n                day = min(relative_date.day, calendar.monthrange(year, month)[1])\n                return datetime(year, month, day)\n            else:\n                # Base the day to valid range on the target month\n                target_month = relative_date.month + ord\n                day = min(relative_date.day, calendar.monthrange(relative_date.year, target_month)[1])\n                return datetime(relative_date.year, target_month, day)\n        elif time == 'end of the':\n            return datetime(\n                relative_date.year,\n                relative_date.month,\n                calendar.monthrange(relative_date.year, relative_date.month)[1]\n            )\n    elif dow in week_variations:\n        if time == 'this':\n            return relative_date - timedelta(days=relative_date.weekday())\n        elif time == 'last' or time == 'previous':\n            return relative_date - timedelta(weeks=1)\n        elif time == 'next' or time == 'following':\n            return relative_date + timedelta(weeks=ord)\n        elif time == 'end of the':\n            day_of_week = base_date.weekday()\n            return day_of_week + timedelta(days=6 - relative_date.weekday())\n    elif dow in day_variations:\n        if time == 'this':\n            return relative_date\n        elif time == 'last' or time == 'previous':\n            return relative_date - timedelta(days=1)\n        elif time == 'next' or time == 'following':\n            return relative_date + timedelta(days=ord)\n        elif time == 'end of the':\n            return datetime(relative_date.year, relative_date.month, relative_date.day, 23, 59, 59)\n\n\ndef date_from_adverb(base_date: datetime, name: str) -> datetime:\n    \"\"\"\n    Convert Day adverbs to dates\n    Tomorrow => Date\n    Today => Date\n    \"\"\"\n    # Reset date to start of the day\n    adverb_date = datetime(base_date.year, base_date.month, base_date.day)\n    if name == 'today' or name == 'tonite' or name == 'tonight':\n        return adverb_date\n    elif name == 'yesterday':\n        return adverb_date - timedelta(days=1)\n    elif name == 'tomorrow' or name == 'tom':\n        return adverb_date + timedelta(days=1)\n\n\ndef date_from_duration(base_date: datetime, number_as_string: str, unit: str, duration: str, base_time: str = None) -> datetime:\n    \"\"\"\n    Find dates from duration\n    Eg: 20 days from now\n    Currently does not support strings like \"20 days from last monday\".\n    \"\"\"\n    # Check if query is `2 days before yesterday` or `day before yesterday`\n    if base_time is not None:\n        base_date = date_from_adverb(base_date, base_time)\n    num = convert_string_to_number(number_as_string)\n    if unit in day_variations:\n        args = {'days': num}\n    elif unit in minute_variations:\n        args = {'minutes': num}\n    elif unit in week_variations:\n        args = {'weeks': num}\n    elif unit in month_variations:\n        args = {'days': 365 * num / 12}\n    elif unit in year_variations:\n        args = {'years': num}\n    if duration == 'ago' or duration == 'before' or duration == 'earlier':\n        if 'years' in args:\n            return datetime(base_date.year - args['years'], base_date.month, base_date.day)\n        return base_date - timedelta(**args)\n    elif duration == 'after' or duration == 'later' or duration == 'from now':\n        if 'years' in args:\n            return datetime(base_date.year + args['years'], base_date.month, base_date.day)\n        return base_date + timedelta(**args)\n\n\ndef this_week_day(base_date: datetime, weekday: int) -> datetime:\n    \"\"\"\n    Finds coming weekday\n    \"\"\"\n    day_of_week = base_date.weekday()\n    # If today is Tuesday and the query is `this monday`\n    # We should output the next_week monday\n    if day_of_week > weekday:\n        return next_week_day(base_date, weekday)\n    start_of_this_week = base_date - timedelta(days=day_of_week + 1)\n    day = start_of_this_week + timedelta(days=1)\n    while day.weekday() != weekday:\n        day = day + timedelta(days=1)\n    return day\n\n\ndef previous_week_day(base_date: datetime, weekday: int) -> datetime:\n    \"\"\"\n    Finds previous weekday\n    \"\"\"\n    day = base_date - timedelta(days=1)\n    while day.weekday() != weekday:\n        day = day - timedelta(days=1)\n    return day\n\n\ndef next_week_day(base_date: datetime, weekday: int) -> datetime:\n    \"\"\"\n    Finds the next weekday.\n    \"\"\"\n    day_of_week = base_date.weekday()\n    end_of_this_week = base_date + timedelta(days=6 - day_of_week)\n    day = end_of_this_week + timedelta(days=1)\n    while day.weekday() != weekday:\n        day = day + timedelta(days=1)\n    return day\n\n\ndef datetime_parsing(text: str, base_date: datetime = datetime.now()) -> list[tuple[str, datetime, tuple[int, int]]]:\n    \"\"\"\n    Extract datetime objects from a string of text.\n    \"\"\"\n    matches = []\n    found_array = []\n\n    # Find the position in the string\n    for expression, function in regex:\n        for match in expression.finditer(text):\n            matches.append((match.group(), function(match, base_date), match.span()))\n\n    # Wrap the matched text with TAG element to prevent nested selections\n    for match, value, spans in matches:\n        subn = re.subn(\n            '(?!<TAG[^>]*?>)' + match + '(?![^<]*?</TAG>)', '<TAG>' + match + '</TAG>', text\n        )\n        text = subn[0]\n        is_substituted = subn[1]\n        if is_substituted != 0:\n            found_array.append((match, value, spans))\n\n    # To preserve order of the match, sort based on the start position\n    return sorted(found_array, key=lambda match: match and match[2][0])\n"
  },
  {
    "path": "chatterbot/preprocessors.py",
    "content": "\"\"\"\nStatement pre-processors.\n\"\"\"\nfrom chatterbot.conversation import Statement\nfrom unicodedata import normalize\nfrom re import sub as re_sub\nfrom html import unescape\n\n\ndef clean_whitespace(statement: Statement) -> Statement:\n    \"\"\"\n    Remove any consecutive whitespace characters from the statement text.\n    \"\"\"\n    # Replace linebreaks and tabs with spaces\n    # Uses splitlines() which includes a superset of universal newlines:\n    # https://docs.python.org/3/library/stdtypes.html#str.splitlines\n    statement.text = ' '.join(statement.text.splitlines()).replace('\\t', ' ')\n\n    # Remove any leading or trailing whitespace\n    statement.text = statement.text.strip()\n\n    # Remove consecutive spaces\n    statement.text = re_sub(' +', ' ', statement.text)\n\n    return statement\n\n\ndef unescape_html(statement: Statement) -> Statement:\n    \"\"\"\n    Convert escaped html characters into unescaped html characters.\n    For example: \"&lt;b&gt;\" becomes \"<b>\".\n    \"\"\"\n    statement.text = unescape(statement.text)\n\n    return statement\n\n\ndef convert_to_ascii(statement: Statement) -> Statement:\n    \"\"\"\n    Converts unicode characters to ASCII character equivalents.\n    For example: \"på fédéral\" becomes \"pa federal\".\n    \"\"\"\n    text = normalize('NFKD', statement.text)\n    text = text.encode('ascii', 'ignore').decode('utf-8')\n\n    statement.text = str(text)\n    return statement\n"
  },
  {
    "path": "chatterbot/response_selection.py",
    "content": "\"\"\"\nResponse selection methods determines which response should be used in\nthe event that multiple responses are generated within a logic adapter.\n\"\"\"\nfrom chatterbot.conversation import Statement\nimport logging\n\n\ndef get_most_frequent_response(input_statement: Statement, response_list: list[Statement], storage=None) -> Statement:\n    \"\"\"\n    :param input_statement: A statement, that closely matches an input to the chat bot.\n\n    :param response_list: A list of statement options to choose a response from.\n\n    :param storage: An instance of a storage adapter to allow the response selection\n                    method to access other statements if needed.\n    :type storage: StorageAdapter\n\n    :return: The response statement with the greatest number of occurrences.\n    \"\"\"\n    logger = logging.getLogger(__name__)\n    logger.info('Selecting response with greatest number of occurrences.')\n\n    # Collect all unique text values from response_list\n    response_texts = set(statement.text for statement in response_list)\n\n    # Fetch all statements matching the input in a single query\n    # Then count occurrences in memory\n    all_matching = list(storage.filter(in_response_to=input_statement.text))\n\n    # Count how many times each response text appears in the database\n    occurrence_counts = {}\n    for statement in all_matching:\n        if statement.text in response_texts:\n            occurrence_counts[statement.text] = occurrence_counts.get(statement.text, 0) + 1\n\n    # Find the response with the highest occurrence count\n    matching_response = None\n    occurrence_count = -1\n\n    for statement in response_list:\n        count = occurrence_counts.get(statement.text, 0)\n\n        # Keep the more common statement\n        if count >= occurrence_count:\n            matching_response = statement\n            occurrence_count = count\n\n    # Choose the most commonly occurring matching response\n    return matching_response\n\n\ndef get_first_response(input_statement: Statement, response_list: list[Statement], storage=None) -> Statement:\n    \"\"\"\n    :param input_statement: A statement, that closely matches an input to the chat bot.\n\n    :param response_list: A list of statement options to choose a response from.\n\n    :param storage: An instance of a storage adapter to allow the response selection\n                    method to access other statements if needed.\n    :type storage: StorageAdapter\n\n    :return: Return the first statement in the response list.\n    \"\"\"\n    logger = logging.getLogger(__name__)\n    logger.info('Selecting first response from list of {} options.'.format(\n        len(response_list)\n    ))\n    return response_list[0]\n\n\ndef get_random_response(input_statement: Statement, response_list: list[Statement], storage=None) -> Statement:\n    \"\"\"\n    :param input_statement: A statement, that closely matches an input to the chat bot.\n    :type input_statement: Statement\n\n    :param response_list: A list of statement options to choose a response from.\n    :type response_list: list\n\n    :param storage: An instance of a storage adapter to allow the response selection\n                    method to access other statements if needed.\n    :type storage: StorageAdapter\n\n    :return: Choose a random response from the selection.\n    \"\"\"\n    from random import choice\n    logger = logging.getLogger(__name__)\n    logger.info('Selecting a response from list of {} options.'.format(\n        len(response_list)\n    ))\n    return choice(response_list)\n"
  },
  {
    "path": "chatterbot/search.py",
    "content": "class IndexedTextSearch:\n    \"\"\"\n    :param statement_comparison_function: A comparison class.\n        Defaults to ``LevenshteinDistance``.\n\n    :param search_page_size:\n        The maximum number of records to load into memory at a time when searching.\n        Defaults to 1000\n    \"\"\"\n\n    name = 'indexed_text_search'\n\n    def __init__(self, chatbot, **kwargs):\n        from chatterbot.comparisons import LevenshteinDistance\n\n        self.chatbot = chatbot\n\n        statement_comparison_function = kwargs.get(\n            'statement_comparison_function',\n            LevenshteinDistance\n        )\n\n        self.compare_statements = statement_comparison_function(\n            language=self.chatbot.tagger.language\n        )\n\n        self.search_page_size = kwargs.get(\n            'search_page_size', 1000\n        )\n\n    def search(self, input_statement, **additional_parameters):\n        \"\"\"\n        Search for close matches to the input. Confidence scores for\n        subsequent results will order of increasing value.\n\n        :param input_statement: A statement.\n        :type input_statement: chatterbot.conversation.Statement\n\n        :param **additional_parameters: Additional parameters to be passed\n            to the ``filter`` method of the storage adapter when searching.\n\n        :rtype: Generator yielding one closest matching statement at a time.\n        \"\"\"\n        self.chatbot.logger.info('Beginning search for close text match')\n\n        search_parameters = {\n            'search_in_response_to_contains': input_statement.search_text,\n            'persona_not_startswith': 'bot:',\n            'page_size': self.search_page_size\n        }\n\n        if additional_parameters:\n            search_parameters.update(additional_parameters)\n\n        statement_list = self.chatbot.storage.filter(**search_parameters)\n\n        best_confidence_so_far = 0\n\n        self.chatbot.logger.info('Processing search results')\n\n        # Find the closest matching known statement\n        for statement in statement_list:\n            confidence = self.compare_statements.compare_text(\n                input_statement.text, statement.in_response_to\n            )\n\n            if confidence > best_confidence_so_far:\n                best_confidence_so_far = confidence\n                statement.confidence = confidence\n\n                self.chatbot.logger.info('Similar text found: {} {}'.format(\n                    statement.in_response_to, confidence\n                ))\n\n                yield statement\n\n                if confidence >= 1.0:\n                    self.chatbot.logger.info('Exact match found, stopping search')\n                    break\n\n\nclass TextSearch:\n    \"\"\"\n    :param statement_comparison_function: A comparison class.\n        Defaults to ``LevenshteinDistance``.\n\n    :param search_page_size:\n        The maximum number of records to load into memory at a time when searching.\n        Defaults to 1000\n    \"\"\"\n\n    name = 'text_search'\n\n    def __init__(self, chatbot, **kwargs):\n        from chatterbot.comparisons import LevenshteinDistance\n\n        self.chatbot = chatbot\n\n        statement_comparison_function = kwargs.get(\n            'statement_comparison_function',\n            LevenshteinDistance\n        )\n\n        self.compare_statements = statement_comparison_function(\n            language=self.chatbot.tagger.language\n        )\n\n        self.search_page_size = kwargs.get(\n            'search_page_size', 1000\n        )\n\n    def search(self, input_statement, **additional_parameters):\n        \"\"\"\n        Search for close matches to the input. Confidence scores for\n        subsequent results will order of increasing value.\n\n        :param input_statement: A statement.\n        :type input_statement: chatterbot.conversation.Statement\n\n        :param **additional_parameters: Additional parameters to be passed\n            to the ``filter`` method of the storage adapter when searching.\n\n        :rtype: Generator yielding one closest matching statement at a time.\n        \"\"\"\n        self.chatbot.logger.info('Beginning search for close text match')\n\n        search_parameters = {\n            'persona_not_startswith': 'bot:',\n            'page_size': self.search_page_size\n        }\n\n        if additional_parameters:\n            search_parameters.update(additional_parameters)\n\n        statement_list = self.chatbot.storage.filter(**search_parameters)\n\n        best_confidence_so_far = 0\n\n        self.chatbot.logger.info('Processing search results')\n\n        # Find the closest matching known statement\n        for statement in statement_list:\n            confidence = self.compare_statements.compare_text(\n                input_statement.text, statement.in_response_to\n            )\n\n            if confidence > best_confidence_so_far:\n                best_confidence_so_far = confidence\n                statement.confidence = confidence\n\n                self.chatbot.logger.info('Similar text found: {} {}'.format(\n                    statement.text, confidence\n                ))\n\n                yield statement\n\n                if confidence >= 1.0:\n                    self.chatbot.logger.info('Exact match found, stopping search')\n                    break\n\n\nclass SemanticVectorSearch:\n    \"\"\"\n    Semantic vector search for storage adapters that use vector embeddings.\n    Does not require a tagger or comparison function - relies on the storage\n    adapter's native vector similarity search capabilities.\n\n    This search algorithm is designed for vector-based storage adapters like\n    RedisVectorStorageAdapter. Unlike indexed text search (IndexedTextSearch)\n    which uses string matching and requires a two-phase search, semantic vector\n    search finds the best matching response in a single phase using vector\n    similarity.\n\n    Architecture differences:\n    -------------------------\n    Indexed Text Search (SQL adapters):\n    - Phase 1: Find statements with string similarity to input\n    - Phase 2: Find variations of the match to get diverse responses\n    - Requires search_text and search_in_response_to indexed fields\n    - Uses comparison functions (Levenshtein, Jaccard, etc.)\n\n    Semantic Vector Search (Redis adapter):\n    - Single phase: Find statements with vector similarity to input\n    - Semantic embeddings capture contextual meaning\n    - No need for Phase 2 - vector similarity already provides the best match\n    - Does not use search_text/search_in_response_to fields\n    - Confidence scores based on cosine distance in vector space\n\n    :param search_page_size:\n        The maximum number of records to load into memory at a time when searching.\n        Defaults to 1000\n    \"\"\"\n\n    name = 'semantic_vector_search'\n\n    def __init__(self, chatbot, **kwargs):\n        self.chatbot = chatbot\n\n        self.search_page_size = kwargs.get(\n            'search_page_size', 1000\n        )\n\n    def search(self, input_statement, **additional_parameters):\n        \"\"\"\n        Search for semantically similar statements using vector similarity.\n        Confidence scores are calculated by the storage adapter based on\n        vector distances and returned in the results.\n\n        :param input_statement: A statement.\n        :type input_statement: chatterbot.conversation.Statement\n\n        :param **additional_parameters: Additional parameters to be passed\n            to the ``filter`` method of the storage adapter when searching.\n\n        :rtype: Generator yielding one closest matching statement at a time.\n        \"\"\"\n        self.chatbot.logger.info('Beginning semantic vector search')\n\n        search_parameters = {\n            'search_in_response_to_contains': input_statement.text,\n            'persona_not_startswith': 'bot:',\n            'page_size': self.search_page_size\n        }\n\n        if additional_parameters:\n            search_parameters.update(additional_parameters)\n\n        statement_list = self.chatbot.storage.filter(**search_parameters)\n\n        best_confidence_so_far = 0\n\n        self.chatbot.logger.info('Processing search results')\n\n        # Yield statements with confidence scores from vector similarity\n        for statement in statement_list:\n            # Confidence is set by the storage adapter during filter()\n            confidence = statement.confidence\n\n            if confidence > best_confidence_so_far:\n                best_confidence_so_far = confidence\n\n                self.chatbot.logger.info('Similar statement found: {} {}'.format(\n                    statement.in_response_to, confidence\n                ))\n\n                yield statement\n\n                if confidence >= 1.0:\n                    self.chatbot.logger.info('Exact match found, stopping search')\n                    break\n"
  },
  {
    "path": "chatterbot/storage/__init__.py",
    "content": "from chatterbot.storage.storage_adapter import StorageAdapter\nfrom chatterbot.storage.django_storage import DjangoStorageAdapter\nfrom chatterbot.storage.mongodb import MongoDatabaseAdapter\nfrom chatterbot.storage.sql_storage import SQLStorageAdapter\nfrom chatterbot.storage.redis import RedisVectorStorageAdapter\n\n\n__all__ = (\n    'StorageAdapter',\n    'DjangoStorageAdapter',\n    'MongoDatabaseAdapter',\n    'SQLStorageAdapter',\n    'RedisVectorStorageAdapter',\n)\n"
  },
  {
    "path": "chatterbot/storage/django_storage.py",
    "content": "from chatterbot.storage import StorageAdapter\nfrom chatterbot import constants\n\n\nclass DjangoStorageAdapter(StorageAdapter):\n    \"\"\"\n    Storage adapter that allows ChatterBot to interact with\n    Django storage backends.\n\n    :param database: The Django database alias to use (default: 'default')\n    :type database: str\n    :param statement_model: The Statement model to use (default: reads from CHATTERBOT_STATEMENT_MODEL setting)\n    :type statement_model: str\n    :param tag_model: The Tag model to use (default: reads from CHATTERBOT_TAG_MODEL setting)\n    :type tag_model: str\n    \"\"\"\n\n    def __init__(self, **kwargs):\n        super().__init__(**kwargs)\n        from django.conf import settings\n\n        self.django_app_name = kwargs.get(\n            'django_app_name',\n            constants.DEFAULT_DJANGO_APP_NAME\n        )\n\n        self.database = kwargs.get('database', 'default')\n\n        # Support custom models via kwargs or Django settings\n        self.statement_model = kwargs.get(\n            'statement_model',\n            getattr(\n                settings,\n                'CHATTERBOT_STATEMENT_MODEL',\n                f'{self.django_app_name}.Statement'\n            )\n        )\n\n        self.tag_model = kwargs.get(\n            'tag_model',\n            getattr(\n                settings,\n                'CHATTERBOT_TAG_MODEL',\n                f'{self.django_app_name}.Tag'\n            )\n        )\n\n    def get_statement_model(self):\n        from django.apps import apps\n        return apps.get_model(self.statement_model)\n\n    def get_tag_model(self):\n        from django.apps import apps\n        return apps.get_model(self.tag_model)\n\n    def count(self) -> int:\n        Statement = self.get_model('statement')\n        return Statement.objects.using(self.database).count()\n\n    def filter(self, **kwargs):\n        \"\"\"\n        Returns a list of statements in the database\n        that match the parameters specified.\n        \"\"\"\n        from django.db.models import Q\n\n        Statement = self.get_model('statement')\n\n        kwargs.pop('page_size', 1000)\n        order_by = kwargs.pop('order_by', None)\n        tags = kwargs.pop('tags', [])\n        exclude_text = kwargs.pop('exclude_text', None)\n        exclude_text_words = kwargs.pop('exclude_text_words', [])\n        persona_not_startswith = kwargs.pop('persona_not_startswith', None)\n        search_text_contains = kwargs.pop('search_text_contains', None)\n        search_in_response_to_contains = kwargs.pop('search_in_response_to_contains', None)\n\n        # Convert a single sting into a list if only one tag is provided\n        if isinstance(tags, str):\n            tags = [tags]\n\n        if tags:\n            kwargs['tags__name__in'] = tags\n\n        statements = Statement.objects.using(self.database).filter(**kwargs)\n\n        if exclude_text:\n            statements = statements.exclude(\n                text__in=exclude_text\n            )\n\n        if exclude_text_words:\n            or_query = [\n                ~Q(text__icontains=word) for word in exclude_text_words\n            ]\n\n            statements = statements.filter(\n                *or_query\n            )\n\n        if persona_not_startswith:\n            statements = statements.exclude(\n                persona__startswith='bot:'\n            )\n\n        if search_text_contains:\n            or_query = Q()\n\n            for word in search_text_contains.split(' '):\n                or_query |= Q(search_text__contains=word)\n\n            statements = statements.filter(\n                or_query\n            )\n\n        if search_in_response_to_contains:\n            or_query = Q()\n\n            for word in search_in_response_to_contains.split(' '):\n                or_query |= Q(search_in_response_to__contains=word)\n\n            statements = statements.filter(\n                or_query\n            )\n\n        if order_by:\n            statements = statements.order_by(*order_by)\n\n        for statement in statements.iterator():\n            yield statement\n\n    def create(self, **kwargs):\n        \"\"\"\n        Creates a new statement matching the keyword arguments specified.\n        Returns the created statement.\n        \"\"\"\n        Statement = self.get_model('statement')\n        Tag = self.get_model('tag')\n\n        tags = kwargs.pop('tags', [])\n\n        if 'search_in_response_to' in kwargs and kwargs['search_in_response_to'] is None:\n            kwargs['search_in_response_to'] = ''\n\n        statement = Statement(**kwargs)\n\n        statement.save(using=self.database)\n\n        tags_to_add = []\n\n        for _tag in tags:\n            tag, _ = Tag.objects.using(self.database).get_or_create(name=_tag)\n            tags_to_add.append(tag)\n\n        statement.tags.add(*tags_to_add)\n\n        return statement\n\n    def create_many(self, statements):\n        \"\"\"\n        Creates multiple statement entries.\n        \"\"\"\n        Statement = self.get_model('statement')\n        Tag = self.get_model('tag')\n\n        tag_cache = {}\n\n        for statement in statements:\n\n            statement_data = statement.serialize()\n            tag_data = statement_data.pop('tags', [])\n\n            statement_model_object = Statement(**statement_data)\n\n            statement_model_object.save(using=self.database)\n\n            tags_to_add = []\n\n            for tag_name in tag_data:\n                if tag_name in tag_cache:\n                    tag = tag_cache[tag_name]\n                else:\n                    tag, _ = Tag.objects.using(self.database).get_or_create(name=tag_name)\n                    tag_cache[tag_name] = tag\n                tags_to_add.append(tag)\n\n            statement_model_object.tags.add(*tags_to_add)\n\n    def update(self, statement):\n        \"\"\"\n        Update the provided statement.\n        \"\"\"\n        Statement = self.get_model('statement')\n        Tag = self.get_model('tag')\n\n        if hasattr(statement, 'id'):\n            statement.save(using=self.database)\n        else:\n            statement = Statement.objects.using(self.database).create(\n                text=statement.text,\n                search_text=statement.search_text,\n                conversation=statement.conversation,\n                in_response_to=statement.in_response_to,\n                search_in_response_to=statement.search_in_response_to or '',\n                created_at=statement.created_at\n            )\n\n        for _tag in statement.tags.all():\n            tag, _ = Tag.objects.using(self.database).get_or_create(name=_tag)\n\n            statement.tags.add(tag)\n\n        return statement\n\n    def get_random(self):\n        \"\"\"\n        Returns a random statement from the database\n        \"\"\"\n        Statement = self.get_model('statement')\n\n        statement = Statement.objects.using(self.database).order_by('?').first()\n\n        if statement is None:\n            raise self.EmptyDatabaseException()\n\n        return statement\n\n    def remove(self, statement_text):\n        \"\"\"\n        Removes the statement that matches the input text.\n        Removes any responses from statements if the response text matches the\n        input text.\n        \"\"\"\n        Statement = self.get_model('statement')\n\n        statements = Statement.objects.using(self.database).filter(text=statement_text)\n\n        statements.delete()\n\n    def drop(self):\n        \"\"\"\n        Remove all data from the database.\n        \"\"\"\n        Statement = self.get_model('statement')\n        Tag = self.get_model('tag')\n\n        Statement.objects.using(self.database).all().delete()\n        Tag.objects.using(self.database).all().delete()\n"
  },
  {
    "path": "chatterbot/storage/mongodb.py",
    "content": "import re\nfrom random import randint\nfrom chatterbot.storage import StorageAdapter\n\n\nclass MongoDatabaseAdapter(StorageAdapter):\n    \"\"\"\n    The MongoDatabaseAdapter is an interface that allows\n    ChatterBot to store statements in a MongoDB database.\n\n    :keyword database_uri: The URI of a remote instance of MongoDB.\n                           This can be any valid\n                           `MongoDB connection string <https://docs.mongodb.com/manual/reference/connection-string/>`_\n    :type database_uri: str\n\n    :keyword mongodb_client_kwargs: Additional keyword arguments to pass to the MongoClient constructor.\n                                    This can include SSL/TLS settings, authentication options, and other\n                                    PyMongo client configuration parameters.\n    :type mongodb_client_kwargs: dict\n\n    .. code-block:: python\n\n       # Basic connection\n       database_uri='mongodb://example.com:8100/'\n\n       # Connection with SSL/TLS (e.g., Amazon DocumentDB)\n       database_uri='mongodb://USER:PASSWORD@my.cluster.us-west-x.docdb.amazonaws.com:27017/?ssl=true&replicaSet=rs0',\n       mongodb_client_kwargs={\n           'tlsCAFile': 'path/to/rds-combined-ca-bundle.pem'\n       }\n    \"\"\"\n\n    def __init__(self, **kwargs):\n        super().__init__(**kwargs)\n        from pymongo import MongoClient\n        from pymongo.errors import OperationFailure\n\n        self.database_uri = kwargs.get(\n            'database_uri', 'mongodb://localhost:27017/chatterbot-database'\n        )\n\n        # Extract additional MongoClient parameters\n        mongodb_client_kwargs = kwargs.get('mongodb_client_kwargs', {})\n\n        # Use the default host and port with additional client parameters\n        self.client = MongoClient(self.database_uri, **mongodb_client_kwargs)\n\n        # Increase the sort buffer to 42M if possible\n        try:\n            self.client.admin.command({'setParameter': 1, 'internalQueryExecMaxBlockingSortBytes': 44040192})\n        except OperationFailure:\n            pass\n\n        # Specify the name of the database\n        self.database = self.client.get_database()\n\n        # The mongo collection of statement documents\n        self.statements = self.database['statements']\n\n    def get_statement_model(self):\n        \"\"\"\n        Return the class for the statement model.\n        \"\"\"\n        from chatterbot.conversation import Statement\n\n        # Create a storage-aware statement\n        statement = Statement\n        statement.storage = self\n\n        return statement\n\n    def count(self) -> int:\n        return self.statements.count_documents({})\n\n    def mongo_to_object(self, statement_data):\n        \"\"\"\n        Return Statement object when given data\n        returned from Mongo DB.\n        \"\"\"\n        Statement = self.get_model('statement')\n\n        statement_data['id'] = statement_data['_id']\n\n        return Statement(**statement_data)\n\n    def filter(self, **kwargs):\n        \"\"\"\n        Returns a list of statements in the database\n        that match the parameters specified.\n        \"\"\"\n        import pymongo\n\n        page_size = kwargs.pop('page_size', 1000)\n        order_by = kwargs.pop('order_by', None)\n        tags = kwargs.pop('tags', [])\n        exclude_text = kwargs.pop('exclude_text', None)\n        exclude_text_words = kwargs.pop('exclude_text_words', [])\n        persona_not_startswith = kwargs.pop('persona_not_startswith', None)\n        search_text_contains = kwargs.pop('search_text_contains', None)\n        search_in_response_to_contains = kwargs.pop('search_in_response_to_contains', None)\n\n        if tags:\n            kwargs['tags'] = {\n                '$in': tags\n            }\n\n        if exclude_text:\n            if 'text' not in kwargs:\n                kwargs['text'] = {}\n            elif 'text' in kwargs and isinstance(kwargs['text'], str):\n                text = kwargs.pop('text')\n                kwargs['text'] = {\n                    '$eq': text\n                }\n            kwargs['text']['$nin'] = exclude_text\n\n        if exclude_text_words:\n            if 'text' not in kwargs:\n                kwargs['text'] = {}\n            elif 'text' in kwargs and isinstance(kwargs['text'], str):\n                text = kwargs.pop('text')\n                kwargs['text'] = {\n                    '$eq': text\n                }\n            exclude_word_regex = '|'.join([\n                '.*{}.*'.format(word) for word in exclude_text_words\n            ])\n            kwargs['text']['$not'] = re.compile(exclude_word_regex)\n\n        if persona_not_startswith:\n            if 'persona' not in kwargs:\n                kwargs['persona'] = {}\n            elif 'persona' in kwargs and isinstance(kwargs['persona'], str):\n                persona = kwargs.pop('persona')\n                kwargs['persona'] = {\n                    '$eq': persona\n                }\n            kwargs['persona']['$not'] = re.compile('^bot:*')\n\n        if search_text_contains:\n            or_regex = '|'.join([\n                '{}'.format(re.escape(word)) for word in search_text_contains.split(' ')\n            ])\n            kwargs['search_text'] = re.compile(or_regex)\n\n        if search_in_response_to_contains:\n            or_regex = '|'.join([\n                '{}'.format(re.escape(word)) for word in search_in_response_to_contains.split(' ')\n            ])\n            kwargs['search_in_response_to'] = re.compile(or_regex)\n\n        mongo_ordering = []\n\n        if order_by:\n\n            # Sort so that newer datetimes appear first\n            if 'created_at' in order_by:\n                order_by.remove('created_at')\n                mongo_ordering.append(('created_at', pymongo.DESCENDING, ))\n\n            for order in order_by:\n                mongo_ordering.append((order, pymongo.ASCENDING))\n\n        # Build the query cursor\n        if mongo_ordering:\n            cursor = self.statements.find(kwargs).sort(mongo_ordering)\n        else:\n            cursor = self.statements.find(kwargs)\n\n        # Use batch_size for efficient pagination without counting total documents\n        cursor = cursor.batch_size(page_size)\n\n        for match in cursor:\n            yield self.mongo_to_object(match)\n\n    def create(self, **kwargs):\n        \"\"\"\n        Creates a new statement matching the keyword arguments specified.\n        Returns the created statement.\n        \"\"\"\n        Statement = self.get_model('statement')\n\n        if 'tags' in kwargs:\n            kwargs['tags'] = list(set(kwargs['tags']))\n\n        inserted = self.statements.insert_one(kwargs)\n\n        kwargs['id'] = inserted.inserted_id\n\n        return Statement(**kwargs)\n\n    def create_many(self, statements):\n        \"\"\"\n        Creates multiple statement entries.\n        \"\"\"\n        create_statements = []\n\n        for statement in statements:\n            statement_data = statement.serialize()\n            tag_data = list(set(statement_data.pop('tags', [])))\n            statement_data['tags'] = tag_data\n\n            create_statements.append(statement_data)\n\n        self.statements.insert_many(create_statements)\n\n    def update(self, statement):\n        data = statement.serialize()\n        data.pop('id', None)\n        data.pop('tags', None)\n\n        update_data = {\n            '$set': data\n        }\n\n        if statement.tags:\n            update_data['$addToSet'] = {\n                'tags': {\n                    '$each': statement.tags\n                }\n            }\n\n        search_parameters = {}\n\n        if statement.id is not None:\n            search_parameters['_id'] = statement.id\n        else:\n            search_parameters['text'] = statement.text\n            search_parameters['conversation'] = statement.conversation\n\n        update_operation = self.statements.update_one(\n            search_parameters,\n            update_data,\n            upsert=True\n        )\n\n        if update_operation.acknowledged:\n            statement.id = update_operation.upserted_id\n\n        return statement\n\n    def get_random(self):\n        \"\"\"\n        Returns a random statement from the database\n        \"\"\"\n        count = self.count()\n\n        if count < 1:\n            raise self.EmptyDatabaseException()\n\n        random_integer = randint(0, count - 1)\n\n        statements = self.statements.find().limit(1).skip(random_integer)\n\n        return self.mongo_to_object(list(statements)[0])\n\n    def remove(self, statement_text):\n        \"\"\"\n        Removes the statement that matches the input text.\n        \"\"\"\n        self.statements.delete_one({'text': statement_text})\n\n    def drop(self):\n        \"\"\"\n        Remove the database.\n        \"\"\"\n        self.client.drop_database(self.database.name)\n\n    def close(self):\n        \"\"\"\n        Close the MongoDB client connection.\n        \"\"\"\n        if hasattr(self, 'client'):\n            self.client.close()\n"
  },
  {
    "path": "chatterbot/storage/redis.py",
    "content": "from datetime import datetime\nimport json\nimport re\nfrom chatterbot.storage import StorageAdapter\nfrom chatterbot.conversation import Statement as StatementObject\n\n\ndef _escape_redis_special_characters(text):\n    \"\"\"\n    Escape special characters in a string that are used in redis queries.\n\n    This function escapes characters that would interfere with the query syntax\n    used in the filter() method, specifically:\n    - Pipe (|) which is used as the OR operator when joining search terms\n    - Characters that could break the wildcard pattern matching\n    \"\"\"\n    from redisvl.query.filter import TokenEscaper\n\n    # Remove space (last character) and add pipe\n    escape_pattern = TokenEscaper.DEFAULT_ESCAPED_CHARS.rstrip(' ]') + r'\\|]'\n\n    escaper = TokenEscaper(escape_chars_re=re.compile(escape_pattern))\n    return escaper.escape(text)\n\n\nclass RedisVectorStorageAdapter(StorageAdapter):\n    \"\"\"\n    .. warning:: BETA feature (Released March, 2025): this storage adapter is new\n        and experimental. Its functionality and default parameters might change\n        in the future and its behavior has not yet been finalized.\n\n    The RedisVectorStorageAdapter allows ChatterBot to store conversation\n    data in a redis instance using vector embeddings for semantic similarity search.\n\n    All parameters are optional, by default a redis instance on localhost is assumed.\n\n    :keyword database_uri: eg: redis://localhost:6379/0',\n        The database_uri can be specified to choose a redis instance.\n    :type database_uri: str\n\n    :keyword embedding_model: The name of the embedding model to use.\n        Default: 'sentence-transformers/all-mpnet-base-v2' (768-dim, balanced speed/quality).\n        Alternatives: 'all-MiniLM-L6-v2' (384-dim, faster), 'multi-qa-mpnet-base-dot-v1' (768-dim, Q&A optimized),\n        'paraphrase-multilingual-mpnet-base-v2' (768-dim, multilingual).\n    :type embedding_model: str\n\n    :keyword embedding_provider: The embedding provider to use. Options: 'huggingface' (default),\n        'openai', 'cohere'. Requires corresponding packages (langchain-openai, langchain-cohere).\n    :type embedding_provider: str\n\n    :keyword embedding_kwargs: Additional keyword arguments to pass to the embedding provider.\n        For HuggingFace: model_kwargs (device, torch_dtype), encode_kwargs (normalize_embeddings, batch_size).\n        For OpenAI: model name (e.g., 'text-embedding-3-small'), dimensions.\n        For Cohere: model name (e.g., 'embed-english-v3.0').\n    :type embedding_kwargs: dict\n\n    Architecture:\n    -------------\n    Unlike SQL storage adapters that use indexed text fields (search_text,\n    search_in_response_to) for string-based similarity matching, Redis uses\n    vector embeddings for semantic similarity. The 'in_response_to' field is\n    embedded as a vector, enabling the system to find statements that respond\n    to semantically similar inputs.\n\n    When used with SemanticVectorSearch, this adapter returns the best matching\n    response directly from Phase 1 search. The semantic vector similarity already\n    captures contextual closeness, making the traditional Phase 2 variation search\n    (used in indexed text search) redundant.\n\n    For SQL with indexed text search:\n\n    - Phase 1 finds a match based on string similarity (Levenshtein distance)\n    - Phase 2 finds variations of that match to get diverse responses\n    - This makes sense because you might have multiple instances of similar statements\n      learned from different conversations that provide different response options\n\n    For Redis with semantic vectors:\n\n    - Phase 1 finds semantically similar responses using vector embeddings\n    - The semantic similarity already captures the \"closeness\" we want\n    - Phase 2 would be redundant - we already have the best semantic match\n    - The vector search inherently considers the entire semantic space, not just\n      exact string matches, so additional variation searching is unnecessary\n\n    NOTES:\n\n    * Unlike other database based storage adapters, the RedisVectorStorageAdapter\n      does not leverage `search_text` and `search_in_response_to` fields for indexing.\n      Instead, it uses vector embeddings to find similar statements based on\n      semantic similarity. This allows for more flexible and context-aware matching.\n    \"\"\"\n\n    class RedisMetaDataType:\n        \"\"\"\n        Subclass for redis config metadata type enumerator.\n        \"\"\"\n        TAG = 'tag'\n        TEXT = 'text'\n        NUMERIC = 'numeric'\n\n    def __init__(self, **kwargs):\n        super().__init__(**kwargs)\n        from chatterbot.vectorstores import RedisVectorStore\n        from langchain_redis import RedisConfig\n\n        self.database_uri = kwargs.get('database_uri', 'redis://localhost:6379/0')\n\n        # https://reference.langchain.com/python/integrations/langchain_redis/\n        config = RedisConfig(\n            index_name='chatterbot',\n            redis_url=self.database_uri,\n            content_field='in_response_to',\n            legacy_key_format=False,\n            metadata_schema=[\n                {\n                    'name': 'conversation',\n                    'type': self.RedisMetaDataType.TAG,\n                },\n                {\n                    'name': 'text',\n                    'type': self.RedisMetaDataType.TEXT,\n                },\n                {\n                    'name': 'created_at',\n                    'type': self.RedisMetaDataType.NUMERIC,\n                },\n                {\n                    'name': 'persona',\n                    'type': self.RedisMetaDataType.TEXT,\n                },\n                {\n                    'name': 'tags',\n                    'type': self.RedisMetaDataType.TAG,\n                    # 'separator': '|'\n                },\n            ],\n        )\n\n        # Configure embedding model\n        embedding_provider = kwargs.get('embedding_provider', 'huggingface').lower()\n        embedding_model = kwargs.get(\n            'embedding_model',\n            'sentence-transformers/all-mpnet-base-v2'\n        )\n        embedding_kwargs = kwargs.get('embedding_kwargs', {})\n\n        self.logger.info(f'Loading {embedding_provider} embeddings: {embedding_model}')\n\n        # Initialize embeddings based on provider\n        if embedding_provider == 'huggingface':\n            from langchain_huggingface import HuggingFaceEmbeddings\n            embeddings = HuggingFaceEmbeddings(\n                model_name=embedding_model,\n                **embedding_kwargs\n            )\n        elif embedding_provider == 'openai':\n            try:\n                from langchain_openai import OpenAIEmbeddings\n                embeddings = OpenAIEmbeddings(\n                    model=embedding_model,\n                    **embedding_kwargs\n                )\n            except ImportError:\n                raise ImportError(\n                    \"OpenAI embeddings require 'langchain-openai' package. \"\n                    \"Install with: pip install langchain-openai\"\n                )\n        elif embedding_provider == 'cohere':\n            try:\n                from langchain_cohere import CohereEmbeddings\n                embeddings = CohereEmbeddings(\n                    model=embedding_model,\n                    **embedding_kwargs\n                )\n            except ImportError:\n                raise ImportError(\n                    \"Cohere embeddings require 'langchain-cohere' package. \"\n                    \"Install with: pip install langchain-cohere\"\n                )\n        else:\n            raise ValueError(\n                f\"Unsupported embedding provider: {embedding_provider}. \"\n                \"Supported providers: 'huggingface', 'openai', 'cohere'\"\n            )\n\n        self.logger.info('Creating Redis Vector Store')\n\n        self.vector_store = RedisVectorStore(embeddings, config=config)\n\n    def get_preferred_tagger(self):\n        \"\"\"\n        Redis uses vector embeddings and doesn't need POS-lemma indexing.\n        Returns NoOpTagger to avoid unnecessary spaCy processing.\n        \"\"\"\n        from chatterbot.tagging import NoOpTagger\n        return NoOpTagger\n\n    def get_preferred_search_algorithm(self):\n        \"\"\"\n        Redis uses semantic vector search instead of text-based matching.\n        Returns the name of the SemanticVectorSearch algorithm.\n        \"\"\"\n        return 'semantic_vector_search'\n\n    def get_statement_model(self):\n        \"\"\"\n        Return the statement model.\n        \"\"\"\n        from langchain_core.documents import Document\n\n        # Add the extra_statement_field_names attribute expected by StorageAdapter\n        if not hasattr(Document, 'extra_statement_field_names'):\n            Document.extra_statement_field_names = []\n\n        return Document\n\n    def _calculate_confidence_from_distance(self, distance):\n        \"\"\"\n        Convert Redis cosine distance to confidence score.\n\n        :param distance: Cosine distance from Redis (0 = identical, 2 = opposite)\n        :return: Confidence score (1.0 = identical, 0.0 = opposite)\n        \"\"\"\n        if distance is not None:\n            return max(0.0, 1.0 - (float(distance) / 2.0))\n        return 0.0\n\n    def _add_confidence_to_results(self, results):\n        \"\"\"\n        Add confidence scores to similarity search results.\n\n        :param results: List of (document, distance) tuples from similarity_search_with_score\n        :return: List of documents with confidence in metadata\n        \"\"\"\n        documents = []\n        for doc, distance in results:\n            doc.metadata['confidence'] = self._calculate_confidence_from_distance(distance)\n            documents.append(doc)\n        return documents\n\n    def model_to_object(self, document):\n\n        in_response_to = document.page_content\n\n        # If the value is an empty string, set it to None\n        # to match the expected type (the vector store does\n        # not use null values)\n        if in_response_to == '':\n            in_response_to = None\n\n        values = {\n            'in_response_to': in_response_to,\n        }\n\n        if document.id:\n            values['id'] = document.id\n\n        values.update(document.metadata)\n\n        # Convert Unix timestamp back to datetime for StatementObject\n        # Redis may return this as int, float, or string representation\n        if 'created_at' in values:\n            created_at_value = values['created_at']\n            if isinstance(created_at_value, str):\n                # Convert string to float first\n                created_at_value = float(created_at_value)\n            if isinstance(created_at_value, (int, float)):\n                values['created_at'] = datetime.fromtimestamp(created_at_value)\n\n        tags = values['tags']\n        values['tags'] = list(set(tags.split('|') if tags else []))\n\n        return StatementObject(**values)\n\n    def count(self) -> int:\n        \"\"\"\n        Return the number of statement entries in the database.\n        \"\"\"\n        index_name = self.vector_store.config.index_name\n        client = self.vector_store.index.client\n\n        # Count only keys matching the ChatterBot index prefix\n        count = 0\n        for _ in client.scan_iter(f'{index_name}:*'):\n            count += 1\n        return count\n\n    def remove(self, statement):\n        \"\"\"\n        Removes the statement that matches the input text.\n        Removes any responses from statements where the response text matches\n        the input text.\n        \"\"\"\n        client = self.vector_store.index.client\n        client.delete(statement.id)\n\n    def filter(self, page_size=4, **kwargs):\n        \"\"\"\n        Returns a list of objects from the database.\n        The kwargs parameter can contain any number\n        of attributes. Only objects which contain all\n        listed attributes and in which all values match\n        for all listed attributes will be returned.\n\n        kwargs:\n            - conversation\n            - persona\n            - tags\n            - in_response_to\n            - text\n            - exclude_text\n            - exclude_text_words\n            - persona_not_startswith\n            - search_text_contains\n            - search_in_response_to_contains\n            - order_by\n        \"\"\"\n        from redisvl.query.filter import Tag, Text\n\n        # https://redis.io/docs/latest/develop/interact/search-and-query/advanced-concepts/query_syntax/\n        filter_condition = None\n\n        ordering = kwargs.get('order_by', None)\n\n        if ordering:\n            ordering = ','.join(ordering)\n\n        if 'in_response_to' in kwargs:\n            filter_condition = Text('in_response_to') == kwargs['in_response_to']\n\n        if 'conversation' in kwargs:\n            query = Tag('conversation') == kwargs['conversation']\n            if filter_condition:\n                filter_condition &= query\n            else:\n                filter_condition = query\n\n        if 'persona' in kwargs:\n            query = Tag('persona') == kwargs['persona']\n            if filter_condition:\n                filter_condition &= query\n            else:\n                filter_condition = query\n\n        if 'tags' in kwargs:\n            query = Tag('tags') == kwargs['tags']\n            if filter_condition:\n                filter_condition &= query\n            else:\n                filter_condition = query\n\n        if 'exclude_text' in kwargs:\n            query = Text('text') != '|'.join([\n                f'%%{text}%%' for text in kwargs['exclude_text']\n            ])\n            if filter_condition:\n                filter_condition &= query\n            else:\n                filter_condition = query\n\n        if 'exclude_text_words' in kwargs and kwargs['exclude_text_words']:\n            _query = '|'.join([\n                f'%%{text}%%' for text in kwargs['exclude_text_words']\n            ])\n            query = Text('text') % f'-({_query})'\n            if filter_condition:\n                filter_condition &= query\n            else:\n                filter_condition = query\n\n        if 'persona_not_startswith' in kwargs:\n            _query = _escape_redis_special_characters(kwargs['persona_not_startswith'])\n            query = Text('persona') % f'-(%%{_query}%%)'\n            if filter_condition:\n                filter_condition &= query\n            else:\n                filter_condition = query\n\n        if 'text' in kwargs:\n            _query = _escape_redis_special_characters(kwargs['text'])\n            query = Text('text') % '|'.join([f'%%{_q}%%' for _q in _query.split()])\n            if filter_condition:\n                filter_condition &= query\n            else:\n                filter_condition = query\n\n        if 'search_text_contains' in kwargs:\n            # Find statements whose text (responses) are similar.\n            #\n            # Use semantic similarity on the search query itself. This finds responses\n            # that would be semantically appropriate, even if they don't share exact words.\n            #\n            # Our vectors are of 'in_response_to' (what was said TO the bot),\n            # not 'text' (what the bot said). So we use the query as if it were an input,\n            # and find statements that would respond to similar inputs. The result is\n            # statements whose context (in_response_to) is similar, which tends to yield\n            # similar responses.\n            _search_query = kwargs['search_text_contains']\n\n            results = self.vector_store.similarity_search_with_score(\n                _search_query,\n                k=page_size,  # The number of results to return\n                return_all=True,  # Include the full document with IDs\n                filter=filter_condition,\n                sort_by=ordering\n            )\n\n            documents = self._add_confidence_to_results(results)\n            return [self.model_to_object(document) for document in documents]\n\n        # Redis uses vector similarity: we search for statements whose actual\n        # text field is semantically similar to the text that produced this search_text.\n        # This is stored in the closest_match.text field, but BestMatch only passes\n        # search_text. Since we can't reverse POS tags to original text (for now),\n        # we treat this parameter as a signal to do text-based similarity search.\n        #\n        # Note: The caller should ideally pass the actual text, but for compatibility\n        # we'll work with what we receive. In practice, search_text_contains is the\n        # better parameter for this use case.\n        if 'search_text' in kwargs:\n            # For now, we'll treat search_text as a filter-only parameter\n            # and fall through to the regular query_search below.\n            # This prevents the broken behavior of embedding POS tags.\n            # The proper fix requires BestMatch to pass additional context\n            # or use search_text_contains instead.\n            pass\n\n        ordering = kwargs.get('order_by', None)\n\n        if ordering:\n            # Redis can't sort by 'id' (it's the key, not a field)\n            # Use 'created_at' instead which provides chronological ordering\n            ordering = ['created_at' if field == 'id' else field for field in ordering]\n            ordering = ','.join(ordering)\n\n        if 'search_in_response_to_contains' in kwargs:\n            _search_text = kwargs.get('search_in_response_to_contains', '')\n\n            results = self.vector_store.similarity_search_with_score(\n                _search_text,\n                k=page_size,  # The number of results to return\n                return_all=True,  # Include the full document with IDs\n                filter=filter_condition,\n                sort_by=ordering\n            )\n\n            documents = self._add_confidence_to_results(results)\n        else:\n            documents = self.vector_store.query_search(\n                k=page_size,\n                filter=filter_condition,\n                sort_by=ordering\n            )\n\n        return [self.model_to_object(document) for document in documents]\n\n    def create(\n        self,\n        text,\n        in_response_to=None,\n        tags=None,\n        **kwargs\n    ):\n        \"\"\"\n        Creates a new statement matching the keyword arguments specified.\n        Returns the created statement.\n        \"\"\"\n        # from langchain_community.vectorstores.redis.constants import REDIS_TAG_SEPARATOR\n\n        _default_date = datetime.now()\n\n        # Prevent duplicate tag entries in the database\n        unique_tags = list(set(tags)) if tags else []\n\n        # Handle created_at: convert datetime to timestamp if needed\n        created_at_value = kwargs.get('created_at')\n        if isinstance(created_at_value, datetime):\n            created_at_timestamp = created_at_value.timestamp()\n        elif created_at_value:\n            created_at_timestamp = created_at_value\n        else:\n            created_at_timestamp = _default_date.timestamp()\n\n        metadata = {\n            'text': text,\n            'category': kwargs.get('category', ''),\n            # Store created_at as Unix timestamp with microseconds (float)\n            # This provides full datetime precision while maintaining Redis NUMERIC field compatibility\n            'created_at': created_at_timestamp,\n            'tags': '|'.join(unique_tags) if unique_tags else '',\n            'conversation': kwargs.get('conversation', ''),\n            'persona': kwargs.get('persona', ''),\n        }\n\n        ids = self.vector_store.add_texts([in_response_to or ''], [metadata])\n\n        metadata['created_at'] = _default_date\n        metadata['tags'] = unique_tags\n        metadata.pop('text')\n        statement = StatementObject(\n            id=ids[0],\n            text=text,\n            in_response_to=in_response_to,\n            **metadata\n        )\n        return statement\n\n    def create_many(self, statements):\n        \"\"\"\n        Creates multiple statement entries.\n        \"\"\"\n        Document = self.get_statement_model()\n        documents = [\n            Document(\n                page_content=statement.in_response_to or '',\n                metadata={\n                    'text': statement.text,\n                    'conversation': statement.conversation or '',\n                    'created_at': statement.created_at.timestamp(),\n                    'persona': statement.persona or '',\n                    # Prevent duplicate tag entries in the database\n                    'tags': '|'.join(\n                        list(set(statement.tags))\n                    ) if statement.tags else '',\n                }\n            ) for statement in statements\n        ]\n\n        self.logger.info('Adding documents to the vector store')\n\n        self.vector_store.add_documents(documents)\n\n    def update(self, statement):\n        \"\"\"\n        Modifies an entry in the database.\n        Creates an entry if one does not exist.\n        \"\"\"\n        # Prevent duplicate tag entries in the database\n        unique_tags = list(set(statement.tags)) if statement.tags else []\n\n        metadata = {\n            'text': statement.text,\n            'conversation': statement.conversation or '',\n            'created_at': statement.created_at.timestamp(),\n            'persona': statement.persona or '',\n            'tags': '|'.join(unique_tags) if unique_tags else '',\n        }\n\n        Document = self.get_statement_model()\n        document = Document(\n            page_content=statement.in_response_to or '',\n            metadata=metadata,\n        )\n\n        if statement.id:\n            # When updating with an existing ID, first delete the old entry\n            # to ensure a duplicate entry is not created\n            client = self.vector_store.index.client\n            client.delete(statement.id)\n\n            # Extract the key from the full ID (format: prefix:key)\n            if ':' in statement.id:\n                key = statement.id.split(':', 1)[1]\n            else:\n                # If no delimiter found, use the entire ID as the key\n                key = statement.id\n\n            ids = self.vector_store.add_texts(\n                [document.page_content], [metadata], keys=[key]\n            )\n        else:\n            self.vector_store.add_documents([document])\n\n    def get_random(self):\n        \"\"\"\n        Returns a random statement from the database.\n        \"\"\"\n        client = self.vector_store.index.client\n\n        random_key = client.randomkey()\n\n        if random_key:\n            # Get the hash data from Redis\n            data = client.hgetall(random_key)\n\n            if data and b'_metadata_json' in data:\n                # Parse the metadata\n                metadata = json.loads(data[b'_metadata_json'].decode())\n\n                # Convert created_at from Unix timestamp back to datetime\n                if 'created_at' in metadata and isinstance(metadata['created_at'], (int, float)):\n                    metadata['created_at'] = datetime.fromtimestamp(metadata['created_at'])\n\n                # Get the in_response_to from the hash\n                in_response_to = data.get(b'in_response_to', b'').decode()\n\n                # Create a Document-like object to use with model_to_object\n                Document = self.get_statement_model()\n                document = Document(\n                    page_content=in_response_to if in_response_to else '',\n                    metadata=metadata,\n                    id=random_key.decode()\n                )\n\n                return self.model_to_object(document)\n\n        raise self.EmptyDatabaseException()\n\n    def drop(self):\n        \"\"\"\n        Remove all existing documents from the database.\n        \"\"\"\n        index_name = self.vector_store.config.index_name\n        client = self.vector_store.index.client\n\n        for key in client.scan_iter(f'{index_name}:*'):\n            # self.vector_store.index.drop_keys(key)\n            client.delete(key)\n\n        # Commenting this out for now because there is no step\n        # to recreate the index after it is dropped (really what\n        # we want is to delete all the keys in the index, but\n        # keep the index itself)\n        # self.vector_store.index.delete(drop=True)\n\n    def close(self):\n        \"\"\"\n        Close the Redis client connection.\n        \"\"\"\n        if hasattr(self, 'vector_store') and hasattr(self.vector_store, 'index'):\n            if hasattr(self.vector_store.index, 'client'):\n                self.vector_store.index.client.close()\n"
  },
  {
    "path": "chatterbot/storage/sql_storage.py",
    "content": "import random\nfrom chatterbot.storage import StorageAdapter\n\n\nclass SQLStorageAdapter(StorageAdapter):\n    \"\"\"\n    The SQLStorageAdapter allows ChatterBot to store conversation\n    data in any database supported by the SQL Alchemy ORM.\n\n    All parameters are optional, by default a sqlite database is used.\n\n    It will check if tables are present, if they are not, it will attempt\n    to create the required tables.\n\n    :keyword database_uri: eg: sqlite:///database_test.sqlite3',\n        The database_uri can be specified to choose database driver.\n    :type database_uri: str\n    \"\"\"\n\n    def __init__(self, **kwargs):\n        super().__init__(**kwargs)\n\n        from sqlalchemy import create_engine, inspect, event\n        from sqlalchemy import Index\n        from sqlalchemy.engine import Engine\n        from sqlalchemy.orm import sessionmaker, scoped_session\n\n        self.database_uri = kwargs.get('database_uri', False)\n\n        # None results in a sqlite in-memory database as the default\n        if self.database_uri is None:\n            self.database_uri = 'sqlite://'\n\n        # Create a file database if the database is not a connection string\n        if not self.database_uri:\n            self.database_uri = 'sqlite:///db.sqlite3'\n\n        # Configure connection pool with safe defaults to prevent exhaustion\n        # Note: SQLite uses SingletonThreadPool which doesn't support these params\n        # PostgreSQL, MySQL, etc. use QueuePool which does support them\n        pool_config = {}\n\n        if self.database_uri.startswith('sqlite://'):\n\n            @event.listens_for(Engine, 'connect')\n            def set_sqlite_pragma(dbapi_connection, connection_record):\n                \"\"\"\n                Set SQLite PRAGMA settings.\n\n                This function is called when a new connection is created.\n                WAL mode must be set outside of a transaction because it cannot\n                change into wal mode from within a transaction.\n                \"\"\"\n                cursor = dbapi_connection.cursor()\n\n                # Check current journal mode\n                cursor.execute('PRAGMA journal_mode')\n                current_mode = cursor.fetchone()[0]\n\n                # Only change if not already in WAL mode\n                if current_mode.lower() != 'wal':\n                    # Set isolation_level to None to execute outside transaction\n                    old_isolation = dbapi_connection.isolation_level\n                    dbapi_connection.isolation_level = None\n                    cursor.execute('PRAGMA journal_mode=WAL')\n                    dbapi_connection.isolation_level = old_isolation\n\n                # Set synchronous mode (can be done normally)\n                cursor.execute('PRAGMA synchronous=NORMAL')\n                cursor.close()\n\n        else:\n            # Only apply pool configuration for databases that support QueuePool\n            # pool_size: Maximum persistent connections (10)\n            # max_overflow: Additional connections during peak load (20)\n            # pool_timeout: Seconds to wait for connection before error (30)\n            # pool_recycle: Recycle connections after 1 hour to prevent stale connections\n            # pool_pre_ping: Test connections before using to detect disconnects\n            pool_config = {\n                'pool_size': kwargs.get('pool_size', 10),\n                'max_overflow': kwargs.get('max_overflow', 20),\n                'pool_timeout': kwargs.get('pool_timeout', 30),\n                'pool_recycle': kwargs.get('pool_recycle', 3600),\n                'pool_pre_ping': kwargs.get('pool_pre_ping', True),\n            }\n\n        self.engine = create_engine(self.database_uri, **pool_config)\n\n        if not inspect(self.engine).has_table('statement'):\n            self.create_database()\n\n        # Check if the expected index exists on the text field of the statement table\n        if not inspect(self.engine).has_index('statement', 'idx_cb_search_text'):\n            from chatterbot.ext.sqlalchemy_app.models import Statement\n\n            search_text_index = Index(\n                'idx_cb_search_text',\n                Statement.search_text\n            )\n\n            search_text_index.create(bind=self.engine)\n\n        # Check if the expected index exists on the in_response_to field of the statement table\n        if not inspect(self.engine).has_index('statement', 'idx_cb_search_in_response_to'):\n            from chatterbot.ext.sqlalchemy_app.models import Statement\n\n            search_in_response_to_index = Index(\n                'idx_cb_search_in_response_to',\n                Statement.search_in_response_to\n            )\n\n            search_in_response_to_index.create(bind=self.engine)\n\n        # Use a scoped session for thread-safe session management\n        # This provides thread-local session storage to prevent session sharing across threads\n        session_factory = sessionmaker(bind=self.engine, expire_on_commit=True)\n        self.Session = scoped_session(session_factory)\n\n    def get_statement_model(self):\n        \"\"\"\n        Return the statement model.\n        \"\"\"\n        from chatterbot.ext.sqlalchemy_app.models import Statement\n        return Statement\n\n    def get_tag_model(self):\n        \"\"\"\n        Return the conversation model.\n        \"\"\"\n        from chatterbot.ext.sqlalchemy_app.models import Tag\n        return Tag\n\n    def model_to_object(self, statement):\n        from chatterbot.conversation import Statement as StatementObject\n\n        return StatementObject(**statement.serialize())\n\n    def count(self) -> int:\n        \"\"\"\n        Return the number of entries in the database.\n        \"\"\"\n        Statement = self.get_model('statement')\n\n        session = self.Session()\n        try:\n            statement_count = session.query(Statement).count()\n            return statement_count\n        finally:\n            session.close()\n\n    def remove(self, statement_text):\n        \"\"\"\n        Removes the statement that matches the input text.\n        Removes any responses from statements where the response text matches\n        the input text.\n        \"\"\"\n        Statement = self.get_model('statement')\n        session = self.Session()\n        try:\n            query = session.query(Statement).filter_by(text=statement_text)\n            record = query.first()\n\n            session.delete(record)\n            session.commit()\n        finally:\n            session.close()\n\n    def filter(self, **kwargs):\n        \"\"\"\n        Returns a list of objects from the database.\n        The kwargs parameter can contain any number\n        of attributes. Only objects which contain all\n        listed attributes and in which all values match\n        for all listed attributes will be returned.\n        \"\"\"\n        from sqlalchemy import or_\n\n        Statement = self.get_model('statement')\n        Tag = self.get_model('tag')\n\n        page_size = kwargs.pop('page_size', 1000)\n        order_by = kwargs.pop('order_by', None)\n        tags = kwargs.pop('tags', [])\n        exclude_text = kwargs.pop('exclude_text', None)\n        exclude_text_words = kwargs.pop('exclude_text_words', [])\n        persona_not_startswith = kwargs.pop('persona_not_startswith', None)\n        search_text_contains = kwargs.pop('search_text_contains', None)\n        search_in_response_to_contains = kwargs.pop('search_in_response_to_contains', None)\n\n        # Convert a single sting into a list if only one tag is provided\n        if isinstance(tags, str):\n            tags = [tags]\n\n        # Use context manager to ensure session cleanup even if generator is partially consumed\n        session = self.Session()\n        try:\n            if len(kwargs) == 0:\n                statements = session.query(Statement).filter()\n            else:\n                statements = session.query(Statement).filter_by(**kwargs)\n\n            if tags:\n                statements = statements.join(Statement.tags).filter(\n                    Tag.name.in_(tags)\n                )\n\n            if exclude_text:\n                statements = statements.filter(\n                    ~Statement.text.in_(exclude_text)\n                )\n\n            if exclude_text_words:\n                or_word_query = [\n                    Statement.text.ilike('%' + word + '%') for word in exclude_text_words\n                ]\n                statements = statements.filter(\n                    ~or_(*or_word_query)\n                )\n\n            if persona_not_startswith:\n                statements = statements.filter(\n                    ~Statement.persona.startswith('bot:')\n                )\n\n            if search_text_contains:\n                or_query = [\n                    Statement.search_text.contains(word) for word in search_text_contains.split(' ')\n                ]\n                statements = statements.filter(\n                    or_(*or_query)\n                )\n\n            if search_in_response_to_contains:\n                or_query = [\n                    Statement.search_in_response_to.contains(word) for word in search_in_response_to_contains.split(' ')\n                ]\n                statements = statements.filter(\n                    or_(*or_query)\n                )\n\n            if order_by:\n\n                if 'created_at' in order_by:\n                    index = order_by.index('created_at')\n                    order_by[index] = Statement.created_at.asc()\n\n                statements = statements.order_by(*order_by)\n\n            total_statements = statements.count()\n\n            for start_index in range(0, total_statements, page_size):\n                for statement in statements.slice(start_index, start_index + page_size):\n                    yield self.model_to_object(statement)\n        finally:\n            # Always close session, even if generator is abandoned or exception occurs\n            session.close()\n\n    def create(\n        self,\n        text,\n        in_response_to=None,\n        tags=None,\n        search_text=None,\n        search_in_response_to=None,\n        **kwargs\n    ):\n        \"\"\"\n        Creates a new statement matching the keyword arguments specified.\n        Returns the created statement.\n        \"\"\"\n        Statement = self.get_model('statement')\n        Tag = self.get_model('tag')\n\n        session = self.Session()\n\n        if search_text is None:\n            if self.raise_on_missing_search_text:\n                raise Exception('generate a search_text value')\n\n        if search_in_response_to is None and in_response_to is not None:\n            if self.raise_on_missing_search_text:\n                raise Exception('generate a search_in_response_to value')\n\n        statement = Statement(\n            text=text,\n            in_response_to=in_response_to,\n            search_text=search_text,\n            search_in_response_to=search_in_response_to,\n            **kwargs\n        )\n\n        tags = frozenset(tags) if tags else frozenset()\n\n        # Batch query tags\n        if tags:\n            existing_tags = session.query(Tag).filter(Tag.name.in_(tags)).all()\n            existing_tag_dict = {tag.name: tag for tag in existing_tags}\n\n            for tag_name in tags:\n                tag = existing_tag_dict.get(tag_name)\n                if not tag:\n                    # Create the tag if it doesn't exist\n                    tag = Tag(name=tag_name)\n                statement.tags.append(tag)\n\n        session.add(statement)\n\n        session.commit()\n\n        session.refresh(statement)\n\n        statement_object = self.model_to_object(statement)\n\n        session.close()\n\n        return statement_object\n\n    def create_many(self, statements):\n        \"\"\"\n        Creates multiple statement entries.\n        \"\"\"\n        Statement = self.get_model('statement')\n        Tag = self.get_model('tag')\n\n        session = self.Session()\n\n        create_statements = []\n        create_tags = {}\n\n        # Check if any statements already have a search text\n        have_search_text = any(statement.search_text for statement in statements)\n\n        # Generate search text values in bulk\n        if not have_search_text:\n            if self.raise_on_missing_search_text:\n                raise Exception('generate bulk_search_text values')\n\n        for statement in statements:\n\n            statement_data = statement.serialize()\n            tag_data = statement_data.pop('tags', [])\n\n            statement_model_object = Statement(**statement_data)\n\n            new_tags = set(tag_data) - set(create_tags.keys())\n\n            if new_tags:\n                existing_tags = session.query(Tag).filter(\n                    Tag.name.in_(new_tags)\n                )\n\n                for existing_tag in existing_tags:\n                    create_tags[existing_tag.name] = existing_tag\n\n            for tag_name in tag_data:\n                if tag_name in create_tags:\n                    tag = create_tags[tag_name]\n                else:\n                    # Create the tag if it does not exist\n                    tag = Tag(name=tag_name)\n\n                    create_tags[tag_name] = tag\n\n                statement_model_object.tags.append(tag)\n            create_statements.append(statement_model_object)\n\n        try:\n            session.add_all(create_statements)\n            session.commit()\n        finally:\n            session.close()\n\n    def update(self, statement):\n        \"\"\"\n        Modifies an entry in the database.\n        Creates an entry if one does not exist.\n        \"\"\"\n        Statement = self.get_model('statement')\n        Tag = self.get_model('tag')\n\n        session = self.Session()\n        try:\n            record = None\n\n            if hasattr(statement, 'id') and statement.id is not None:\n                record = session.get(Statement, statement.id)\n            else:\n                record = session.query(Statement).filter(\n                    Statement.text == statement.text,\n                    Statement.conversation == statement.conversation,\n                ).first()\n\n                # Create a new statement entry if one does not already exist\n                if not record:\n                    record = Statement(\n                        text=statement.text,\n                        conversation=statement.conversation,\n                        persona=statement.persona\n                    )\n\n            # Update the response value\n            record.in_response_to = statement.in_response_to\n\n            record.created_at = statement.created_at\n\n            if not statement.search_text:\n                if self.raise_on_missing_search_text:\n                    raise Exception('update issued without search_text value')\n\n            if statement.in_response_to and not statement.search_in_response_to:\n                if self.raise_on_missing_search_text:\n                    raise Exception('update issued without search_in_response_to value')\n\n            for tag_name in statement.get_tags():\n                tag = session.query(Tag).filter_by(name=tag_name).first()\n\n                if not tag:\n                    # Create the record\n                    tag = Tag(name=tag_name)\n\n                record.tags.append(tag)\n\n            session.add(record)\n            session.commit()\n        finally:\n            session.close()\n\n    def get_random(self):\n        \"\"\"\n        Returns a random statement from the database.\n        \"\"\"\n        Statement = self.get_model('statement')\n\n        session = self.Session()\n        try:\n            count = self.count()\n            if count < 1:\n                raise self.EmptyDatabaseException()\n\n            random_index = random.randrange(0, count)\n            random_statement = session.query(Statement)[random_index]\n\n            statement = self.model_to_object(random_statement)\n\n            return statement\n        finally:\n            session.close()\n\n    def drop(self):\n        \"\"\"\n        Drop the database.\n        \"\"\"\n        Statement = self.get_model('statement')\n        Tag = self.get_model('tag')\n\n        session = self.Session()\n        try:\n            session.query(Statement).delete()\n            session.query(Tag).delete()\n\n            session.commit()\n        finally:\n            session.close()\n\n    def create_database(self):\n        \"\"\"\n        Populate the database with the tables.\n        \"\"\"\n        from chatterbot.ext.sqlalchemy_app.models import Base\n        Base.metadata.create_all(self.engine)\n\n    def close(self):\n        \"\"\"\n        Close the database connection and dispose of the engine.\n        This ensures proper cleanup of resources.\n        \"\"\"\n        # Remove thread-local sessions from scoped_session registry\n        if hasattr(self, 'Session'):\n            self.Session.remove()\n\n        # Dispose of the connection pool\n        if hasattr(self, 'engine'):\n            self.engine.dispose()\n"
  },
  {
    "path": "chatterbot/storage/storage_adapter.py",
    "content": "import logging\n\n\nclass StorageAdapter(object):\n    \"\"\"\n    This is an abstract class that represents the interface\n    that all storage adapters should implement.\n    \"\"\"\n\n    def __init__(self, *args, **kwargs):\n        \"\"\"\n        Initialize common attributes shared by all storage adapters.\n\n        :param str tagger_language: The language that the tagger uses to remove stopwords.\n        \"\"\"\n        self.logger = kwargs.get('logger', logging.getLogger(__name__))\n\n        self.raise_on_missing_search_text = kwargs.get(\n            'raise_on_missing_search_text', True\n        )\n\n    def get_model(self, model_name):\n        \"\"\"\n        Return the model class for a given model name.\n\n        model_name is case insensitive.\n        \"\"\"\n        get_model_method = getattr(self, 'get_%s_model' % (\n            model_name.lower(),\n        ))\n\n        return get_model_method()\n\n    def get_object(self, object_name):\n        \"\"\"\n        Return the class for a given object name.\n\n        object_name is case insensitive.\n        \"\"\"\n        get_model_method = getattr(self, 'get_%s_object' % (\n            object_name.lower(),\n        ))\n\n        return get_model_method()\n\n    def get_statement_object(self):\n        from chatterbot.conversation import Statement\n\n        StatementModel = self.get_model('statement')\n\n        Statement.statement_field_names.extend(\n            StatementModel.extra_statement_field_names\n        )\n\n        return Statement\n\n    def count(self) -> int:\n        \"\"\"\n        Return the number of entries in the database.\n        \"\"\"\n        raise self.AdapterMethodNotImplementedError(\n            'The `count` method is not implemented by this adapter.'\n        )\n\n    def remove(self, statement_text):\n        \"\"\"\n        Removes the statement that matches the input text.\n        Removes any responses from statements where the response text matches\n        the input text.\n        \"\"\"\n        raise self.AdapterMethodNotImplementedError(\n            'The `remove` method is not implemented by this adapter.'\n        )\n\n    def filter(self, **kwargs):\n        \"\"\"\n        Returns a list of objects from the database.\n        The kwargs parameter can contain any number\n        of attributes. Only objects which contain\n        all listed attributes and in which all values\n        match for all listed attributes will be returned.\n\n        :param page_size: The maximum number of records to load into\n            memory at once when returning results.\n            Defaults to 1000\n\n        :param order_by: The field name that should be used to determine\n            the order that results are returned in.\n            Defaults to None\n\n        :param tags: A list of tags. When specified, the results will only\n            include statements that have a tag in the provided list.\n            Defaults to [] (empty list)\n\n        :param exclude_text: If the ``text`` of a statement is an exact match\n            for the value of this parameter the statement will not be\n            included in the result set.\n            Defaults to None\n\n        :param exclude_text_words: If the ``text`` of a statement contains a\n            word from this list then the statement will not be included in\n            the result set.\n            Defaults to [] (empty list)\n\n        :param persona_not_startswith: If the ``persona`` field of a\n            statement starts with the value specified by this parameter,\n            then the statement will not be returned in the result set.\n            Defaults to None\n\n        :param search_text_contains: If the ``search_text`` field of a\n            statement contains a word that is in the string provided to\n            this parameter, then the statement will be included in the\n            result set.\n            Defaults to None\n\n        :param search_in_response_to: If the ``search_in_response_to`` field\n            of a statement contains a word that is in the string provided to\n            this parameter, then the statement will be included in the\n            result set.\n            Defaults to None\n        \"\"\"\n        raise self.AdapterMethodNotImplementedError(\n            'The `filter` method is not implemented by this adapter.'\n        )\n\n    def create(self, **kwargs):\n        \"\"\"\n        Creates a new statement matching the keyword arguments specified.\n        Returns the created statement.\n        \"\"\"\n        raise self.AdapterMethodNotImplementedError(\n            'The `create` method is not implemented by this adapter.'\n        )\n\n    def create_many(self, statements):\n        \"\"\"\n        Creates multiple statement entries.\n        \"\"\"\n        raise self.AdapterMethodNotImplementedError(\n            'The `create_many` method is not implemented by this adapter.'\n        )\n\n    def update(self, statement):\n        \"\"\"\n        Modifies an entry in the database.\n        Creates an entry if one does not exist.\n        \"\"\"\n        raise self.AdapterMethodNotImplementedError(\n            'The `update` method is not implemented by this adapter.'\n        )\n\n    def get_random(self):\n        \"\"\"\n        Returns a random statement from the database.\n        \"\"\"\n        raise self.AdapterMethodNotImplementedError(\n            'The `get_random` method is not implemented by this adapter.'\n        )\n\n    def drop(self):\n        \"\"\"\n        Drop the database attached to a given adapter.\n        \"\"\"\n        raise self.AdapterMethodNotImplementedError(\n            'The `drop` method is not implemented by this adapter.'\n        )\n\n    def close(self):\n        \"\"\"\n        Close any open connections or sessions.\n        This method should be called when the storage adapter is no longer needed\n        to properly clean up resources and avoid resource warnings.\n        \"\"\"\n        pass\n\n    def get_preferred_tagger(self):\n        \"\"\"\n        Returns the tagger class preferred by this storage adapter.\n        Returns None by default, meaning the default tagger will be used.\n\n        Storage adapters should override this method to specify their\n        preferred tagger based on their search capabilities.\n\n        Available Taggers:\n\n        - NoOpTagger: Returns text unchanged (for vector-based storage).\n          No spaCy model loading (~500MB memory saved).\n          Faster startup (<1 second vs 2-5 seconds).\n          Use when storage handles semantic search natively.\n\n        - PosLemmaTagger: Creates POS-lemma bigrams (default, for SQL).\n          Enables pattern matching (e.g., \"NOUN:cat VERB:run\").\n          Requires spaCy language model.\n          Best for exact phrase matching.\n\n        - LowercaseTagger: Simple lowercase transformation.\n          Minimal processing overhead.\n          Case-insensitive matching.\n\n        Example - Vector Storage::\n\n            def get_preferred_tagger(self):\n                from chatterbot.tagging import NoOpTagger\n                return NoOpTagger\n\n        Example - Traditional Storage::\n\n            def get_preferred_tagger(self):\n                return None  # Use default PosLemmaTagger\n\n        :return: Tagger class or None\n        \"\"\"\n        return None\n\n    def get_preferred_search_algorithm(self):\n        \"\"\"\n        Returns the search algorithm name preferred by this storage adapter.\n        Returns None by default, meaning the default search algorithm will be used.\n\n        Storage adapters should override this method to specify their\n        preferred search algorithm based on their capabilities.\n\n        Available Search Algorithms:\n\n        - 'indexed_text_search' (default):\n          Uses POS-lemma indexed fields (search_text, search_in_response_to).\n          Python-based Levenshtein distance comparison.\n          Requires PosLemmaTagger.\n          Best for: Exact pattern matching.\n\n        - 'semantic_vector_search':\n          Uses raw text with vector similarity.\n          Delegates to storage.filter(search_in_response_to_contains=text).\n          No tagger required (works with NoOpTagger).\n          Confidence from storage adapter (cosine similarity).\n          Best for: Context-aware AI responses, semantic understanding.\n\n        - 'text_search' (fallback):\n          Compares raw text without indexes.\n          Slower but works with any storage.\n          Uses comparison functions on all statements.\n\n        Example - Vector Storage::\n\n            def get_preferred_search_algorithm(self):\n                return 'semantic_vector_search'\n\n        Example - SQL Storage::\n\n            def get_preferred_search_algorithm(self):\n                return None  # Use default 'indexed_text_search'\n\n        :return: Search algorithm name string or None\n        \"\"\"\n        return None\n\n    class EmptyDatabaseException(Exception):\n\n        def __init__(self, message=None):\n            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.'\n            super().__init__(message or default)\n\n    class AdapterMethodNotImplementedError(NotImplementedError):\n        \"\"\"\n        An exception to be raised when a storage adapter method has not been implemented.\n        Typically this indicates that the method should be implement in a subclass.\n        \"\"\"\n        pass\n"
  },
  {
    "path": "chatterbot/tagging.py",
    "content": "from typing import List, Union, Tuple\nfrom chatterbot import languages\nfrom chatterbot.utils import get_model_for_language\nimport spacy\n\n\nclass NoOpTagger(object):\n    \"\"\"\n    A no-operation tagger that returns text unchanged.\n    Used by storage adapters that don't rely on indexed search_text fields.\n    \"\"\"\n\n    def __init__(self, language=None):\n        self.language = language or languages.ENG\n\n    def needs_text_indexing(self):\n        \"\"\"\n        Indicates whether this tagger performs text indexing/transformation.\n        Returns False since NoOpTagger passes text through unchanged.\n\n        :return: False\n        \"\"\"\n        return False\n\n    def get_text_index_string(self, text: Union[str, List[str]]):\n        \"\"\"\n        Return the text unchanged (no indexing applied).\n        \"\"\"\n        return text\n\n    def as_nlp_pipeline(\n        self,\n        texts: Union[List[str], Tuple[str, dict]],\n        batch_size: int = 1000,\n        n_process: int = 1\n    ):\n        \"\"\"\n        Returns texts unchanged without NLP processing.\n        Maintains API compatibility with other taggers.\n\n        :param texts: Text strings or tuples of (text, context_dict)\n        :param batch_size: Ignored (for API compatibility)\n        :param n_process: Ignored (for API compatibility)\n        \"\"\"\n        process_as_tuples = texts and isinstance(texts[0], tuple)\n\n        if process_as_tuples:\n            # Return generator of (text, context) tuples\n            for text, context in texts:\n                yield (text, context)\n        else:\n            # Return generator of text strings\n            for text in texts:\n                yield text\n\n\nclass LowercaseTagger(object):\n    \"\"\"\n    Returns the text in lowercase.\n    \"\"\"\n\n    def __init__(self, language=None):\n        from chatterbot.components import chatterbot_lowercase_indexer  # noqa\n\n        self.language = language or languages.ENG\n\n        # Create a new empty spacy nlp object\n        self.nlp = spacy.blank(self.language.ISO_639_1)\n\n        self.nlp.add_pipe(\n            'chatterbot_lowercase_indexer', name='chatterbot_lowercase_indexer', last=True\n        )\n\n    def needs_text_indexing(self):\n        \"\"\"\n        Indicates whether this tagger performs text indexing/transformation.\n        Returns True since LowercaseTagger transforms text to lowercase.\n\n        :return: True\n        \"\"\"\n        return True\n\n    def get_text_index_string(self, text: Union[str, List[str]]):\n        if isinstance(text, list):\n            documents = self.nlp.pipe(text, batch_size=1000, n_process=1)\n            return [document._.search_index for document in documents]\n        else:\n            document = self.nlp(text)\n            return document._.search_index\n\n    def as_nlp_pipeline(\n        self,\n        texts: Union[List[str], Tuple[str, dict]],\n        batch_size: int = 1000,\n        n_process: int = 1\n    ):\n        \"\"\"\n        Process texts through the spaCy NLP pipeline with optimized batching.\n\n        :param texts: Text strings or tuples of (text, context_dict)\n        :param batch_size: Number of texts per batch (default 1000)\n        :param n_process: Number of worker processes for spaCy's pipe (set >1 to use multiprocessing)\n\n        Usage:\n            documents = tagger.as_nlp_pipeline(texts)\n            documents = tagger.as_nlp_pipeline(texts, batch_size=2000, n_process=4)\n        \"\"\"\n        process_as_tuples = texts and isinstance(texts[0], tuple)\n\n        documents = self.nlp.pipe(\n            texts,\n            as_tuples=process_as_tuples,\n            batch_size=batch_size,\n            n_process=n_process\n        )\n        return documents\n\n\nclass PosLemmaTagger(object):\n\n    def __init__(self, language=None):\n        from chatterbot.components import chatterbot_bigram_indexer  # noqa\n\n        self.language = language or languages.ENG\n\n        model = get_model_for_language(self.language)\n\n        # Disable the Named Entity Recognition (NER) component because it is not necessary\n        self.nlp = spacy.load(model, exclude=['ner'])\n\n        self.nlp.add_pipe(\n            'chatterbot_bigram_indexer', name='chatterbot_bigram_indexer', last=True\n        )\n\n    def needs_text_indexing(self):\n        \"\"\"\n        Indicates whether this tagger performs text indexing/transformation.\n        Returns True since PosLemmaTagger creates POS-lemma bigram indexes.\n\n        :return: True\n        \"\"\"\n        return True\n\n    def get_text_index_string(self, text: Union[str, List[str]]) -> str:\n        \"\"\"\n        Return a string of text containing part-of-speech, lemma pairs.\n        \"\"\"\n        if isinstance(text, list):\n            documents = self.nlp.pipe(text, batch_size=1000, n_process=1)\n            return [document._.search_index for document in documents]\n        else:\n            document = self.nlp(text)\n            return document._.search_index\n\n    def as_nlp_pipeline(\n        self,\n        texts: Union[List[str], Tuple[str, dict]],\n        batch_size: int = 1000,\n        n_process: int = 1\n    ) -> spacy.tokens.Doc:\n        \"\"\"\n        Accepts a single string or a list of strings, or a list of tuples\n        where the first element is the text and the second element is a\n        dictionary of context to return alongside the generated document.\n\n        :param texts: Text strings or tuples of (text, context_dict)\n        :param batch_size: Number of texts per batch (default 1000)\n        :param n_process: Number of worker processes for spaCy's pipe (set >1 to use multiprocessing)\n\n        Usage:\n            documents = tagger.as_nlp_pipeline(texts)\n            documents = tagger.as_nlp_pipeline(texts, batch_size=2000, n_process=4)\n        \"\"\"\n        process_as_tuples = texts and isinstance(texts[0], tuple)\n\n        documents = self.nlp.pipe(\n            texts,\n            as_tuples=process_as_tuples,\n            batch_size=batch_size,\n            n_process=n_process\n        )\n        return documents\n"
  },
  {
    "path": "chatterbot/trainers.py",
    "content": "import os\nimport csv\nimport time\nimport glob\nimport json\nimport tarfile\nfrom typing import List, Union\nfrom tqdm import tqdm\nfrom dateutil import parser as date_parser\nfrom chatterbot.chatterbot import ChatBot\nfrom chatterbot.conversation import Statement\n\n\nclass Trainer(object):\n    \"\"\"\n    Base class for all other trainer classes.\n\n    :param boolean show_training_progress: Show progress indicators for the\n           trainer. The environment variable ``CHATTERBOT_SHOW_TRAINING_PROGRESS``\n           can also be set to control this. ``show_training_progress`` will override\n           the environment variable if it is set.\n    \"\"\"\n\n    def __init__(self, chatbot: ChatBot, **kwargs):\n        self.chatbot = chatbot\n\n        environment_default = bool(int(os.environ.get('CHATTERBOT_SHOW_TRAINING_PROGRESS', True)))\n\n        self.disable_progress = not kwargs.get(\n            'show_training_progress',\n            environment_default\n        )\n\n    def get_preprocessed_statement(self, input_statement: Statement) -> Statement:\n        \"\"\"\n        Preprocess the input statement.\n        \"\"\"\n        for preprocessor in self.chatbot.preprocessors:\n            input_statement = preprocessor(input_statement)\n\n        return input_statement\n\n    def train(self, *args, **kwargs):\n        \"\"\"\n        This method must be overridden by a child class.\n        \"\"\"\n        raise self.TrainerInitializationException()\n\n    class TrainerInitializationException(Exception):\n        \"\"\"\n        Exception raised when a base class has not overridden\n        the required methods on the Trainer base class.\n        \"\"\"\n\n        def __init__(self, message=None):\n            default = (\n                'A training class must be specified before calling train(). '\n                'See https://docs.chatterbot.us/training/'\n            )\n            super().__init__(message or default)\n\n    def _generate_export_data(self) -> list:\n        result = []\n        for statement in self.chatbot.storage.filter():\n            if statement.in_response_to:\n                result.append([statement.in_response_to, statement.text])\n\n        return result\n\n    def export_for_training(self, file_path='./export.json'):\n        \"\"\"\n        Create a file from the database that can be used to\n        train other chat bots.\n        \"\"\"\n        export = {'conversations': self._generate_export_data()}\n        with open(file_path, 'w+', encoding='utf8') as jsonfile:\n            json.dump(export, jsonfile, ensure_ascii=False)\n\n\nclass ListTrainer(Trainer):\n    \"\"\"\n    Allows a chat bot to be trained using a list of strings\n    where the list represents a conversation.\n    \"\"\"\n\n    def train(self, conversation: List[str]):\n        \"\"\"\n        Train the chat bot based on the provided list of\n        statements that represents a single conversation.\n        \"\"\"\n        previous_statement_text = None\n        previous_statement_search_text = ''\n        statements_to_create = []\n\n        # Preprocess all text before NLP analysis\n        preprocessed_texts = conversation\n        for preprocessor in self.chatbot.preprocessors:\n            preprocessed_texts = [\n                preprocessor(Statement(text=text)).text\n                for text in preprocessed_texts\n            ]\n\n        # Batch process with NLP\n        documents = list(self.chatbot.tagger.as_nlp_pipeline(\n            preprocessed_texts,\n            batch_size=2000,\n            # NOTE: Not all spaCy models support multi-processing\n            n_process=1\n        ))\n\n        # Create statements from processed documents\n        for document in tqdm(documents, desc='List Trainer', disable=self.disable_progress):\n            # Handle both spaCy Doc objects and plain strings from NoOpTagger\n            if isinstance(document, str):\n                # NoOpTagger returns plain strings\n                statement_text = document\n                statement_search_text = self.chatbot.tagger.get_text_index_string(document)\n            else:\n                # Regular taggers return spaCy Doc objects\n                statement_text = document.text\n                statement_search_text = document._.search_index\n\n            statement = Statement(\n                text=statement_text,\n                search_text=statement_search_text,\n                in_response_to=previous_statement_text,\n                search_in_response_to=previous_statement_search_text,\n                conversation='training'\n            )\n\n            previous_statement_text = statement.text\n            previous_statement_search_text = statement_search_text\n            statements_to_create.append(statement)\n\n        self.chatbot.storage.create_many(statements_to_create)\n\n\nclass ChatterBotCorpusTrainer(Trainer):\n    \"\"\"\n    Allows the chat bot to be trained using data from the\n    ChatterBot dialog corpus.\n    \"\"\"\n\n    def train(self, *corpus_paths: Union[str, List[str]]):\n        from chatterbot.corpus import load_corpus, list_corpus_files\n\n        data_file_paths = []\n\n        # Get the paths to each file the bot will be trained with\n        for corpus_path in corpus_paths:\n            data_file_paths.extend(list_corpus_files(corpus_path))\n\n        for corpus, categories, _file_path in tqdm(\n            load_corpus(*data_file_paths),\n            desc='Training corpus',\n            disable=self.disable_progress\n        ):\n            statements_to_create = []\n\n            # Collect all texts from all conversations for batch processing\n            all_texts = []\n            conversation_lengths = []\n\n            for conversation in corpus:\n                conversation_lengths.append(len(conversation))\n                all_texts.extend(conversation)\n\n            # Preprocess all texts\n            preprocessed_texts = all_texts\n            for preprocessor in self.chatbot.preprocessors:\n                preprocessed_texts = [\n                    preprocessor(Statement(text=text)).text\n                    for text in preprocessed_texts\n                ]\n\n            # Batch process all texts with NLP\n            documents = list(self.chatbot.tagger.as_nlp_pipeline(\n                preprocessed_texts,\n                batch_size=2000,\n                # NOTE: Not all spaCy models support multi-processing\n                n_process=1\n            ))\n\n            # Reconstruct conversations from batch-processed documents\n            doc_index = 0\n            for conversation_length in conversation_lengths:\n                previous_statement_text = None\n                previous_statement_search_text = ''\n\n                for _ in range(conversation_length):\n                    document = documents[doc_index]\n                    doc_index += 1\n\n                    # Handle both spaCy Doc objects and plain strings from NoOpTagger\n                    if isinstance(document, str):\n                        # NoOpTagger returns plain strings\n                        statement_text = document\n                        statement_search_text = self.chatbot.tagger.get_text_index_string(document)\n                    else:\n                        # Regular taggers return spaCy Doc objects\n                        statement_text = document.text\n                        statement_search_text = document._.search_index\n\n                    statement = Statement(\n                        text=statement_text,\n                        search_text=statement_search_text,\n                        in_response_to=previous_statement_text,\n                        search_in_response_to=previous_statement_search_text,\n                        conversation='training'\n                    )\n\n                    statement.add_tags(*categories)\n\n                    previous_statement_text = statement.text\n                    previous_statement_search_text = statement_search_text\n                    statements_to_create.append(statement)\n\n            if statements_to_create:\n                self.chatbot.storage.create_many(statements_to_create)\n\n\nclass GenericFileTrainer(Trainer):\n    \"\"\"\n    Allows the chat bot to be trained using data from a CSV or JSON file,\n    or directory of those file types.\n    \"\"\"\n\n    # NOTE: If the value is an integer, this be the\n    # column index instead of the key or header\n    DEFAULT_STATEMENT_TO_HEADER_MAPPING = {\n        'text': 'text',\n        'conversation': 'conversation',\n        'created_at': 'created_at',\n        'persona': 'persona',\n        'tags': 'tags'\n    }\n\n    def __init__(self, chatbot: ChatBot, **kwargs):\n        \"\"\"\n        data_path: str The path to the data file or directory.\n        field_map: dict A dictionary containing the column name to header mapping.\n        \"\"\"\n        super().__init__(chatbot, **kwargs)\n\n        self.file_extension = None\n\n        self.field_map = kwargs.get(\n            'field_map',\n            self.DEFAULT_STATEMENT_TO_HEADER_MAPPING\n        )\n\n    def _get_file_list(self, data_path: str, limit: Union[int, None]):\n        \"\"\"\n        Get a list of files to read from the data set.\n        \"\"\"\n\n        if self.file_extension is None:\n            raise self.TrainerInitializationException(\n                'The file_extension attribute must be set before calling train().'\n            )\n\n        # List all csv or json files in the specified directory\n        if os.path.isdir(data_path):\n            glob_path = os.path.join(data_path, '**', f'*.{self.file_extension}')\n\n            # Use iglob instead of glob for better performance with\n            # large directories because it returns an iterator\n            data_files = glob.iglob(glob_path, recursive=True)\n\n            for index, file_path in enumerate(data_files):\n                if limit is not None and index >= limit:\n                    break\n\n                yield file_path\n        else:\n            yield data_path\n\n    def train(self, data_path: str, limit=None):\n        \"\"\"\n        Train a chatbot with data from the data file.\n\n        :param str data_path: The path to the data file or directory.\n        :param int limit: The maximum number of files to train from.\n        \"\"\"\n\n        if data_path is None:\n            raise self.TrainerInitializationException(\n                'The data_path argument must be set to the path of a file or directory.'\n            )\n\n        data_files = self._get_file_list(data_path, limit)\n\n        files_processed = 0\n\n        for data_file in tqdm(data_files, desc='Training', disable=self.disable_progress):\n\n            previous_statement_text = None\n            previous_statement_search_text = ''\n\n            file_extension = data_file.split('.')[-1].lower()\n\n            statements_to_create = []\n\n            file_abspath = os.path.abspath(data_file)\n\n            with open(file_abspath, 'r', encoding='utf-8') as file:\n\n                if self.file_extension == 'json':\n                    data = json.load(file)\n                    data = data['conversation']\n                elif file_extension == 'csv':\n                    use_header = bool(isinstance(next(iter(self.field_map.values())), str))\n\n                    if use_header:\n                        data = csv.DictReader(file)\n                    else:\n                        data = csv.reader(file)\n                elif file_extension == 'tsv':\n                    use_header = bool(isinstance(next(iter(self.field_map.values())), str))\n\n                    if use_header:\n                        data = csv.DictReader(file, delimiter='\\t')\n                    else:\n                        data = csv.reader(file, delimiter='\\t')\n                else:\n                    self.logger.warning(f'Skipping unsupported file type: {file_extension}')\n                    continue\n\n                files_processed += 1\n\n                text_row = self.field_map['text']\n\n                # Collect all rows first to avoid re-reading file\n                rows_list = [row for row in data if len(row) > 0]\n\n                # Extract text and metadata for each row\n                text_values = []\n                contexts = []\n\n                try:\n                    for row in rows_list:\n                        context = {\n                            key: row[value]\n                            for key, value in self.field_map.items()\n                            if key != text_row\n                        }\n                        contexts.append(context)\n\n                        # Preprocess text\n                        text = row[text_row]\n                        for preprocessor in self.chatbot.preprocessors:\n                            text = preprocessor(Statement(text=text)).text\n\n                        text_values.append((text, context))\n                except KeyError as e:\n                    raise KeyError(\n                        f'{e}. Please check the field_map parameter used to initialize '\n                        f'the training class and remove this value if it is not needed. '\n                        f'Current mapping: {self.field_map}'\n                    )\n\n                # Batch process with NLP\n                documents = self.chatbot.tagger.as_nlp_pipeline(\n                    text_values,\n                    batch_size=2000,\n                    # NOTE: Not all spaCy models support multi-processing\n                    n_process=1\n                )\n\n                # Convert to list for processing\n                documents_list = list(documents)\n\n            response_to_search_index_mapping = {}\n\n            if 'in_response_to' in self.field_map.keys():\n                # Process response references for search indexing\n                in_response_to_field = self.field_map['in_response_to']\n                response_texts = [\n                    row[in_response_to_field]\n                    for row in rows_list\n                    if row[in_response_to_field] is not None\n                ]\n\n                if response_texts:\n                    # Preprocess response texts\n                    preprocessed_response_texts = response_texts\n                    for preprocessor in self.chatbot.preprocessors:\n                        preprocessed_response_texts = [\n                            preprocessor(Statement(text=text)).text\n                            for text in preprocessed_response_texts\n                        ]\n\n                    # Batch process response texts\n                    response_documents = self.chatbot.tagger.as_nlp_pipeline(\n                        preprocessed_response_texts,\n                        batch_size=2000,\n                        # NOTE: Not all spaCy models support multi-processing\n                        n_process=1\n                    )\n\n                    for document in response_documents:\n                        # Handle both spaCy Doc objects and plain strings from NoOpTagger\n                        if isinstance(document, str):\n                            # NoOpTagger returns plain strings\n                            response_to_search_index_mapping[document] = self.chatbot.tagger.get_text_index_string(document)\n                        else:\n                            # Regular taggers return spaCy Doc objects\n                            response_to_search_index_mapping[document.text] = document._.search_index\n\n            # Create statements from processed documents\n            for document, context in tqdm(documents_list, desc='Creating statements', disable=self.disable_progress, leave=False):\n                # Handle both spaCy Doc objects and plain strings from NoOpTagger\n                if isinstance(document, str):\n                    # NoOpTagger returns plain strings\n                    statement_text = document\n                    statement_search_text = self.chatbot.tagger.get_text_index_string(document)\n                else:\n                    # Regular taggers return spaCy Doc objects\n                    statement_text = document.text\n                    statement_search_text = document._.search_index\n                \n                statement = Statement(\n                    text=statement_text,\n                    conversation=context.get('conversation', 'training'),\n                    persona=context.get('persona', None),\n                    tags=context.get('tags', [])\n                )\n\n                if 'created_at' in context:\n                    statement.created_at = date_parser.parse(context['created_at'])\n\n                statement.search_text = statement_search_text\n\n                # Use the in_response_to attribute for the previous statement if\n                # one is defined, otherwise use the last statement which was created\n                if 'in_response_to' in self.field_map.keys():\n                    statement.in_response_to = context.get(self.field_map['in_response_to'], None)\n                    statement.search_in_response_to = response_to_search_index_mapping.get(\n                        context.get(self.field_map['in_response_to'], None), ''\n                    )\n                else:\n                    # List-type data such as CSVs with no response specified can use\n                    # the previous statement as the in_response_to value\n                    statement.in_response_to = previous_statement_text\n                    statement.search_in_response_to = previous_statement_search_text\n\n                previous_statement_text = statement.text\n                previous_statement_search_text = statement.search_text\n\n                statements_to_create.append(statement)\n\n            self.chatbot.storage.create_many(statements_to_create)\n\n        if files_processed:\n            self.chatbot.logger.info(\n                'Training completed. {} files were read.'.format(files_processed)\n            )\n        else:\n            self.chatbot.logger.warning(\n                'No [{}] files were detected at: {}'.format(\n                    self.file_extension,\n                    data_path\n                )\n            )\n\n\nclass CsvFileTrainer(GenericFileTrainer):\n    \"\"\"\n    .. note::\n        Added in version 1.2.4\n\n    Allow chatbots to be trained with data from a CSV file or\n    directory of CSV files.\n\n    TSV files are also supported, as long as the file_extension\n    parameter is set to 'tsv'.\n\n    :param str file_extension: The file extension to look for when searching for files (defaults to 'csv').\n    :param dict field_map: A dictionary containing the database column name to header mapping.\n                          Values can be either the header name (str) or the column index (int).\n    \"\"\"\n\n    def __init__(self, chatbot: ChatBot, **kwargs):\n        super().__init__(chatbot, **kwargs)\n\n        self.file_extension = kwargs.get('file_extension', 'csv')\n\n\nclass JsonFileTrainer(GenericFileTrainer):\n    \"\"\"\n    .. note::\n        Added in version 1.2.4\n\n    Allow chatbots to be trained with data from a JSON file or\n    directory of JSON files.\n\n    :param dict field_map: A dictionary containing the database column name to header mapping.\n    \"\"\"\n\n    DEFAULT_STATEMENT_TO_KEY_MAPPING = {\n        'text': 'text',\n        'conversation': 'conversation',\n        'created_at': 'created_at',\n        'in_response_to': 'in_response_to',\n        'persona': 'persona',\n        'tags': 'tags'\n    }\n\n    def __init__(self, chatbot: ChatBot, **kwargs):\n        super().__init__(chatbot, **kwargs)\n\n        self.file_extension = 'json'\n\n        self.field_map = kwargs.get(\n            'field_map',\n            self.DEFAULT_STATEMENT_TO_KEY_MAPPING\n        )\n\n\nclass UbuntuCorpusTrainer(CsvFileTrainer):\n    \"\"\"\n    .. note::\n        PENDING DEPRECATION: Please use the ``CsvFileTrainer`` for data formats similar to this one.\n\n    Allow chatbots to be trained with the data from the Ubuntu Dialog Corpus.\n\n    For more information about the Ubuntu Dialog Corpus visit:\n    https://dataset.cs.mcgill.ca/ubuntu-corpus-1.0/\n\n    :param str ubuntu_corpus_data_directory: The directory where the Ubuntu corpus data is already located, or where it should be downloaded and extracted.\n    \"\"\"\n\n    def __init__(self, chatbot: ChatBot, **kwargs):\n        super().__init__(chatbot, **kwargs)\n        home_directory = os.path.expanduser('~')\n\n        self.data_download_url = None\n\n        self.data_directory = kwargs.get(\n            'ubuntu_corpus_data_directory',\n            os.path.join(home_directory, 'ubuntu_data')\n        )\n\n        # Directory containing extracted data\n        self.data_path = os.path.join(\n            self.data_directory, 'ubuntu_dialogs'\n        )\n\n        self.field_map = {\n            'text': 3,\n            'created_at': 0,\n            'persona': 1,\n        }\n\n    def is_downloaded(self, file_path: str):\n        \"\"\"\n        Check if the data file is already downloaded.\n        \"\"\"\n        if os.path.exists(file_path):\n            self.chatbot.logger.info('File is already downloaded')\n            return True\n\n        return False\n\n    def is_extracted(self, file_path: str):\n        \"\"\"\n        Check if the data file is already extracted.\n        \"\"\"\n\n        if os.path.isdir(file_path):\n            self.chatbot.logger.info('File is already extracted')\n            return True\n        return False\n\n    def download(self, url: str, show_status=True):\n        \"\"\"\n        Download a file from the given url.\n        Show a progress indicator for the download status.\n        \"\"\"\n        import requests\n\n        # Create the data directory if it does not already exist\n        if not os.path.exists(self.data_directory):\n            os.makedirs(self.data_directory)\n\n        file_name = url.split('/')[-1]\n        file_path = os.path.join(self.data_directory, file_name)\n\n        # Do not download the data if it already exists\n        if self.is_downloaded(file_path):\n            return file_path\n\n        with open(file_path, 'wb') as open_file:\n            if show_status:\n                print('Downloading %s' % url)\n            response = requests.get(url, stream=True)\n            total_length = response.headers.get('content-length')\n\n            if total_length is None:\n                # No content length header\n                open_file.write(response.content)\n            else:\n                for data in tqdm(\n                    response.iter_content(chunk_size=4096),\n                    desc='Downloading',\n                    disable=not show_status\n                ):\n                    open_file.write(data)\n\n        if show_status:\n            print('Download location: %s' % file_path)\n        return file_path\n\n    def extract(self, file_path: str):\n        \"\"\"\n        Extract a tar file at the specified file path.\n        \"\"\"\n        if not self.disable_progress:\n            print('Extracting {}'.format(file_path))\n\n        if not os.path.exists(self.data_path):\n            os.makedirs(self.data_path)\n\n        def is_within_directory(directory, target):\n\n            abs_directory = os.path.abspath(directory)\n            abs_target = os.path.abspath(target)\n\n            prefix = os.path.commonprefix([abs_directory, abs_target])\n\n            return prefix == abs_directory\n\n        def safe_extract(tar, path='.', members=None, *, numeric_owner=False):\n\n            for member in tar.getmembers():\n                member_path = os.path.join(path, member.name)\n                if not is_within_directory(path, member_path):\n                    raise Exception('Attempted Path Traversal in Tar File')\n\n            tar.extractall(path, members, numeric_owner=numeric_owner)\n\n        try:\n            with tarfile.open(file_path, 'r') as tar:\n                safe_extract(tar, path=self.data_path, members=tqdm(tar, disable=self.disable_progress))\n        except tarfile.ReadError as e:\n            raise self.TrainerInitializationException(\n                f'The provided data file is not a valid tar file: {file_path}'\n            ) from e\n\n        self.chatbot.logger.info('File extracted to {}'.format(self.data_path))\n\n        return True\n\n    def _get_file_list(self, data_path: str, limit: Union[int, None]):\n        \"\"\"\n        Get a list of files to read from the data set.\n        \"\"\"\n\n        if self.data_download_url is None:\n            raise self.TrainerInitializationException(\n                'The data_download_url attribute must be set before calling train().'\n            )\n\n        # Download and extract the Ubuntu dialog corpus if needed\n        corpus_download_path = self.download(self.data_download_url)\n\n        # Extract if the directory does not already exist\n        if not self.is_extracted(data_path):\n            self.extract(corpus_download_path)\n\n        extracted_corpus_path = os.path.join(\n            data_path, '**', '**', '*.tsv'\n        )\n\n        # Use iglob instead of glob for better performance with\n        # large directories because it returns an iterator\n        data_files = glob.iglob(extracted_corpus_path)\n\n        for index, file_path in enumerate(data_files):\n            if limit is not None and index >= limit:\n                break\n\n            yield file_path\n\n    def train(self, data_download_url: str, limit: Union[int, None] = None):\n        \"\"\"\n        :param str data_download_url: The URL to download the Ubuntu dialog corpus from.\n        :param int limit: The maximum number of files to train from.\n        \"\"\"\n        self.data_download_url = data_download_url\n\n        start_time = time.time()\n        super().train(self.data_path, limit=limit)\n\n        if not self.disable_progress:\n            print('Training took', time.time() - start_time, 'seconds.')\n"
  },
  {
    "path": "chatterbot/utils.py",
    "content": "\"\"\"\nChatterBot utility functions\n\"\"\"\nfrom typing import Union\nimport importlib\nimport time\n\n\ndef import_module(dotted_path: str):\n    \"\"\"\n    Imports the specified module based on the\n    dot notated import path for the module.\n    \"\"\"\n    module_parts = dotted_path.split('.')\n    module_path = '.'.join(module_parts[:-1])\n    module = importlib.import_module(module_path)\n\n    return getattr(module, module_parts[-1])\n\n\ndef initialize_class(data: Union[dict, str], *args, **kwargs):\n    \"\"\"\n    :param data: A string or dictionary containing a import_path attribute.\n    \"\"\"\n    if isinstance(data, dict):\n        import_path = data.get('import_path')\n        data.update(kwargs)\n        Class = import_module(import_path)\n\n        return Class(*args, **data)\n    else:\n        Class = import_module(data)\n\n        return Class(*args, **kwargs)\n\n\ndef validate_adapter_class(validate_class, adapter_class):\n    \"\"\"\n    Raises an exception if validate_class is not a\n    subclass of adapter_class.\n\n    :param validate_class: The class to be validated.\n    :type validate_class: class\n\n    :param adapter_class: The class type to check against.\n    :type adapter_class: class\n\n    :raises: Adapter.InvalidAdapterTypeException\n    \"\"\"\n    from chatterbot.adapters import Adapter\n\n    # If a dictionary was passed in, check if it has an import_path attribute\n    if isinstance(validate_class, dict):\n\n        if 'import_path' not in validate_class:\n            raise Adapter.InvalidAdapterTypeException(\n                'The dictionary {} must contain a value for \"import_path\"'.format(\n                    str(validate_class)\n                )\n            )\n\n        # Set the class to the import path for the next check\n        validate_class = validate_class.get('import_path')\n\n    if not issubclass(import_module(validate_class), adapter_class):\n        raise Adapter.InvalidAdapterTypeException(\n            '{} must be a subclass of {}'.format(\n                validate_class,\n                adapter_class.__name__\n            )\n        )\n\n\ndef get_response_time(chatbot, statement='Hello') -> float:\n    \"\"\"\n    Returns the amount of time taken for a given\n    chat bot to return a response.\n\n    :param chatbot: A chat bot instance.\n    :type chatbot: ChatBot\n\n    :returns: The response time in seconds.\n    \"\"\"\n    start_time = time.time()\n\n    chatbot.get_response(statement)\n\n    return time.time() - start_time\n\n\ndef get_model_for_language(language):\n    \"\"\"\n    Returns the spacy model for the specified language.\n    \"\"\"\n    from chatterbot import constants\n\n    try:\n        model = constants.DEFAULT_LANGUAGE_TO_SPACY_MODEL_MAP[language]\n    except KeyError as e:\n        if hasattr(language, 'ENGLISH_NAME'):\n            language_name = language.ENGLISH_NAME\n        else:\n            language_name = language\n        raise KeyError(\n            f'A corresponding spacy model for \"{language_name}\" could not be found.'\n        ) from e\n\n    return model\n"
  },
  {
    "path": "chatterbot/vectorstores.py",
    "content": "\"\"\"\nRedis vector store.\n\"\"\"\nfrom __future__ import annotations\n\nfrom typing import List\n\nfrom langchain_core.documents import Document\nfrom redisvl.redis.utils import convert_bytes\nfrom redisvl.query import FilterQuery\n\nfrom langchain_redis.vectorstores import RedisVectorStore as LangChainRedisVectorStore\n\n\nclass RedisVectorStore(LangChainRedisVectorStore):\n    \"\"\"\n    Redis vector store integration.\n    \"\"\"\n\n    def query_search(\n        self,\n        k=4,\n        filter=None,\n        sort_by=None,\n    ) -> List[Document]:\n        \"\"\"\n        Return docs based on the provided query.\n\n        k: int, default=4\n            Number of documents to return.\n        filter: str, default=None\n            A filter expression to apply to the query.\n        sort_by: str, default=None\n            A field to sort the results by.\n\n        returns:\n            A list of Documents most matching the query.\n        \"\"\"\n        from chatterbot import ChatBot\n\n        return_fields = [\n            self.config.content_field\n        ]\n        return_fields += [\n            field.name\n            for field in self._index.schema.fields.values()\n            if field.name\n            not in [self.config.embedding_field, self.config.content_field]\n        ]\n\n        query = FilterQuery(\n            return_fields=return_fields,\n            num_results=k,\n            filter_expression=filter,\n            sort_by=sort_by,\n        )\n\n        try:\n            results = self._index.query(query)\n        except Exception as e:\n            raise ChatBot.ChatBotException(f'Error querying index: {query}') from e\n\n        if results:\n            with self._index.client.pipeline(transaction=False) as pipe:\n                for document in results:\n                    pipe.hgetall(document['id'])\n                full_documents = convert_bytes(pipe.execute())\n        else:\n            full_documents = []\n\n        return self._prepare_docs_full(\n            True, results, full_documents, True\n        )\n"
  },
  {
    "path": "docs/_ext/canonical.py",
    "content": "\"\"\"\nAdd GitHub repository details to the Sphinx context.\n\"\"\"\n\ndef setup_canonical_func(app, pagename, templatename, context, doctree):\n    \"\"\"\n    Return the url to the specified page on GitHub.\n\n    (Sphinx 7.4 generates a canonical link with a .html extension even\n    when run in dirhtml mode)\n    \"\"\"\n\n    conf = app.config\n\n    def canonical_func():\n        # Special case for the root index page\n        if pagename == 'index':\n            return conf.html_baseurl\n\n        dir_name = pagename.replace('/index', '/')\n        return f'{conf.html_baseurl}{dir_name}'\n\n    # Add it to the page's context\n    context['canonical_url'] = canonical_func\n\n\n# Extension setup function\ndef setup(app):\n    app.connect('html-page-context', setup_canonical_func)\n"
  },
  {
    "path": "docs/_ext/github.py",
    "content": "\"\"\"\nAdd GitHub repository details to the Sphinx context.\n\"\"\"\n\nGITHUB_USER = 'gunthercox'\nGITHUB_REPO = 'ChatterBot'\n\n\ndef setup_github_func(app, pagename, templatename, context, doctree):\n    \"\"\"\n    Return the url to the specified page on GitHub.\n    \"\"\"\n\n    github_version = 'master'\n    docs_path = 'docs'\n\n    def my_func():\n        return f'https://github.com/{GITHUB_USER}/{GITHUB_REPO}/blob/{github_version}/{docs_path}/{pagename}.rst'\n\n    # Add it to the page's context\n    context['github_page_link'] = my_func\n\n\n# Extension setup function\ndef setup(app):\n    app.connect('html-page-context', setup_github_func)\n"
  },
  {
    "path": "docs/_includes/python_module_structure.txt",
    "content": "IronyAdapter/\n|── README\n|── pyproject.toml\n|── irony_adapter\n|   |── __init__.py\n|   └── logic.py\n└── tests\n    |── __init__.py\n    └── test_logic.py"
  },
  {
    "path": "docs/_static/mobile.js",
    "content": "/**\n * Mobile Menu and Responsive Enhancements for ChatterBot Documentation\n * Provides mobile-friendly navigation and touch interactions\n */\n\n(function() {\n    'use strict';\n\n    var state = {\n        sidebar: null,\n        toggleButton: null,\n        overlay: null,\n        isInitialized: false,\n        clickOutsideHandler: null,\n        sidebarLinkHandlers: []\n    };\n\n    /**\n     * Create overlay for mobile menu\n     */\n    function createOverlay() {\n        if (state.overlay) return state.overlay;\n\n        var overlay = document.createElement('div');\n        overlay.className = 'mobile-sidebar-overlay';\n        overlay.setAttribute('aria-hidden', 'true');\n        document.body.appendChild(overlay);\n\n        overlay.addEventListener('click', closeMobileMenu);\n\n        return overlay;\n    }\n\n    /**\n     * Open mobile menu\n     */\n    function openMobileMenu() {\n        if (!state.sidebar || !state.toggleButton) return;\n\n        state.sidebar.classList.add('mobile-open');\n        state.overlay.classList.add('active');\n        state.toggleButton.setAttribute('aria-expanded', 'true');\n        state.toggleButton.innerHTML = '✕ Close';\n        document.body.style.overflow = 'hidden'; // Prevent background scrolling\n    }\n\n    /**\n     * Close mobile menu\n     */\n    function closeMobileMenu() {\n        if (!state.sidebar || !state.toggleButton) return;\n\n        state.sidebar.classList.remove('mobile-open');\n        state.overlay.classList.remove('active');\n        state.toggleButton.setAttribute('aria-expanded', 'false');\n        state.toggleButton.innerHTML = '☰ Menu';\n        document.body.style.overflow = ''; // Restore scrolling\n    }\n\n    /**\n     * Toggle mobile menu\n     */\n    function toggleMobileMenu(event) {\n        event.preventDefault();\n        event.stopPropagation();\n\n        var isOpen = state.sidebar.classList.contains('mobile-open');\n\n        if (isOpen) {\n            closeMobileMenu();\n        } else {\n            openMobileMenu();\n        }\n    }\n\n    /**\n     * Initialize mobile menu functionality\n     */\n    function initMobileMenu() {\n        // Only initialize once\n        if (state.isInitialized) return;\n\n        state.sidebar = document.querySelector('div.sphinxsidebar');\n        if (!state.sidebar) return;\n\n        // Create overlay\n        state.overlay = createOverlay();\n\n        // Create mobile menu toggle button\n        state.toggleButton = document.createElement('button');\n        state.toggleButton.className = 'mobile-menu-toggle';\n        state.toggleButton.innerHTML = '☰ Menu';\n        state.toggleButton.setAttribute('aria-label', 'Toggle navigation menu');\n        state.toggleButton.setAttribute('aria-expanded', 'false');\n        state.toggleButton.setAttribute('type', 'button');\n        document.body.appendChild(state.toggleButton);\n\n        // Add click event listener\n        state.toggleButton.addEventListener('click', toggleMobileMenu);\n\n        // Close menu when clicking a link inside sidebar\n        var sidebarLinks = state.sidebar.querySelectorAll('a');\n        sidebarLinks.forEach(function(link) {\n            var handler = function(e) {\n                // Small delay to allow navigation to start\n                setTimeout(closeMobileMenu, 100);\n            };\n            link.addEventListener('click', handler);\n            state.sidebarLinkHandlers.push({ element: link, handler: handler });\n        });\n\n        // Handle escape key\n        document.addEventListener('keydown', function(e) {\n            if (e.key === 'Escape' && state.sidebar.classList.contains('mobile-open')) {\n                closeMobileMenu();\n                state.toggleButton.focus();\n            }\n        });\n\n        state.isInitialized = true;\n    }\n\n    /**\n     * Clean up mobile menu\n     */\n    function cleanupMobileMenu() {\n        if (!state.isInitialized) return;\n\n        // Remove toggle button\n        if (state.toggleButton && state.toggleButton.parentNode) {\n            state.toggleButton.parentNode.removeChild(state.toggleButton);\n        }\n\n        // Remove overlay\n        if (state.overlay && state.overlay.parentNode) {\n            state.overlay.parentNode.removeChild(state.overlay);\n        }\n\n        // Remove sidebar classes\n        if (state.sidebar) {\n            state.sidebar.classList.remove('mobile-open');\n        }\n\n        // Remove event listeners from sidebar links\n        state.sidebarLinkHandlers.forEach(function(item) {\n            item.element.removeEventListener('click', item.handler);\n        });\n\n        // Restore body overflow\n        document.body.style.overflow = '';\n\n        // Reset state\n        state.isInitialized = false;\n        state.toggleButton = null;\n        state.overlay = null;\n        state.sidebarLinkHandlers = [];\n    }\n\n    /**\n     * Improve table responsiveness\n     */\n    function makeTablesResponsive() {\n        var tables = document.querySelectorAll('table.docutils');\n\n        tables.forEach(function(table) {\n            // Skip if already wrapped\n            if (table.parentNode.classList.contains('table-wrapper')) {\n                return;\n            }\n\n            // Create wrapper for horizontal scrolling\n            var wrapper = document.createElement('div');\n            wrapper.className = 'table-wrapper';\n            wrapper.style.overflowX = 'auto';\n            wrapper.style.webkitOverflowScrolling = 'touch';\n            wrapper.style.marginBottom = '1em';\n\n            table.parentNode.insertBefore(wrapper, table);\n            wrapper.appendChild(table);\n        });\n    }\n\n    /**\n     * Add touch-friendly behavior to code blocks\n     */\n    function enhanceCodeBlocks() {\n        var codeBlocks = document.querySelectorAll('div.highlight');\n\n        codeBlocks.forEach(function(block) {\n            // Add visual indicator for scrollable content\n            var pre = block.querySelector('pre');\n            if (pre && pre.scrollWidth > pre.clientWidth) {\n                block.classList.add('scrollable');\n                block.setAttribute('title', 'Swipe to scroll code');\n            }\n        });\n    }\n\n    /**\n     * Handle window resize events\n     */\n    function handleResize() {\n        var isMobile = window.innerWidth <= 480;\n\n        if (isMobile && !state.isInitialized) {\n            // Mobile view - initialize\n            initMobileMenu();\n        } else if (!isMobile && state.isInitialized) {\n            // Desktop view - cleanup\n            cleanupMobileMenu();\n        }\n    }\n\n    /**\n     * Improve accessibility for mobile users\n     */\n    function improveAccessibility() {\n        // Add skip to content link\n        var skipLink = document.createElement('a');\n        skipLink.href = '#document';\n        skipLink.className = 'skip-to-content';\n        skipLink.textContent = 'Skip to content';\n        skipLink.style.position = 'absolute';\n        skipLink.style.top = '-40px';\n        skipLink.style.left = '0';\n        skipLink.style.background = '#300a24';\n        skipLink.style.color = '#e8ffca';\n        skipLink.style.padding = '8px';\n        skipLink.style.textDecoration = 'none';\n        skipLink.style.zIndex = '1001';\n\n        skipLink.addEventListener('focus', function() {\n            this.style.top = '0';\n        });\n\n        skipLink.addEventListener('blur', function() {\n            this.style.top = '-40px';\n        });\n\n        document.body.insertBefore(skipLink, document.body.firstChild);\n    }\n\n    /**\n     * Initialize all mobile enhancements\n     */\n    function init() {\n        // Wait for DOM to be ready\n        if (document.readyState === 'loading') {\n            document.addEventListener('DOMContentLoaded', function() {\n                // Check if mobile on initial load (480px breakpoint for actual phones)\n                if (window.innerWidth <= 480) {\n                    initMobileMenu();\n                }\n                makeTablesResponsive();\n                enhanceCodeBlocks();\n                improveAccessibility();\n            });\n        } else {\n            // Check if mobile on initial load (480px breakpoint for actual phones)\n            if (window.innerWidth <= 480) {\n                initMobileMenu();\n            }\n            makeTablesResponsive();\n            enhanceCodeBlocks();\n            improveAccessibility();\n        }\n\n        // Handle window resize with debouncing\n        var resizeTimer;\n        window.addEventListener('resize', function() {\n            clearTimeout(resizeTimer);\n            resizeTimer = setTimeout(handleResize, 250);\n        });\n\n        // Handle orientation change on mobile devices\n        window.addEventListener('orientationchange', function() {\n            clearTimeout(resizeTimer);\n            resizeTimer = setTimeout(handleResize, 300);\n        });\n    }\n\n    // Initialize\n    init();\n\n})();\n"
  },
  {
    "path": "docs/_static/silktide-consent-manager.css",
    "content": "/* \n  Silktide Consent Manager - https://silktide.com/consent-manager/  \n\n  Styles are at risked of being overridden by styles coming from the site the consent manager is used on.\n  To help prevent this, global wrapper elements are prefixed with \"#silktide-\"\n*/\n\n/* --------------------------------\n  Global Styles - These elements exist in the main DOM and styling is limited to positioning and animation\n-------------------------------- */\n/* Wrapper (Global) */\n#silktide-wrapper {\n              --focus: 0 0 0 2px #ffffff, 0 0 0 4px #000000, 0 0 0 6px #ffffff;\n              --boxShadow: -5px 5px 10px 0px #00000012, 0px 0px 50px 0px #0000001a;\n              --fontFamily: Helvetica Neue, Segoe UI, Arial, sans-serif;\n              --primaryColor: #AFE3FF;\n              --backgroundColor: #070219;\n              --textColor: #E5F6FA;\n              --backdropBackgroundColor: #00000033;\n              --backdropBackgroundBlur: 0px;\n              --cookieIconColor: #070219;\n              --cookieIconBackgroundColor: #AFE3FF;\n              position: fixed;\n  bottom: 0;\n  right: 0;\n  width: 100%;\n  height: 100%;\n  z-index: 99999;\n  pointer-events: none;\n  border: 0px;\n  display: flex;\n  justify-content: center;\n  align-items: center\n            }\n\n/* Backdrop (Global) */\n#silktide-backdrop-global {\n  position: fixed;\n  top: 0;\n  left: 0;\n  width: 100%;\n  height: 100%;\n  pointer-events: auto;\n  border: 0px;\n  display: none;\n}\n\n/* --------------------------------\n  Links\n-------------------------------- */\n#silktide-wrapper a {\n  all: unset;\n  display: inline-block;\n  color: var(--primaryColor);\n  text-decoration: underline;\n}\n\n#silktide-wrapper a:hover {\n  cursor: pointer;\n  color: var(--textColor);\n}\n\n/* --------------------------------\n  Focus Styles\n-------------------------------- */\n#silktide-wrapper a:focus,\n#silktide-wrapper #silktide-banner button:focus,\n#silktide-wrapper #silktide-modal button:focus,\n#silktide-wrapper #silktide-cookie-icon:focus {\n  outline: none;\n  box-shadow: var(--focus);\n  border-radius: 5px;\n}\n\n#silktide-wrapper #silktide-cookie-icon:focus {\n  border-radius: 50%;\n}\n\n/* --------------------------------\n  General Styles\n-------------------------------- */\n\n#silktide-wrapper .st-button {\n  color: var(--backgroundColor);\n  background-color: var(--primaryColor);\n  border: 2px solid var(--primaryColor);\n  padding: 10px 20px;\n  text-decoration: none;\n  text-align: center;\n  display: inline-block;\n  font-size: 16px;\n  line-height: 24px;\n  cursor: pointer;\n  border-radius: 5px;\n}\n\n#silktide-wrapper .st-button--primary {\n}\n\n#silktide-wrapper .st-button--primary:hover {\n  background-color: var(--backgroundColor);\n  color: var(--primaryColor);\n}\n\n#silktide-wrapper .st-button--secondary {\n  background-color: var(--backgroundColor);\n  color: var(--primaryColor);\n}\n\n#silktide-wrapper .st-button--secondary:hover {\n  background-color: var(--primaryColor);\n  color: var(--backgroundColor);\n}\n\n/* --------------------------------\n  Banner\n-------------------------------- */\n#silktide-banner {\n  font-family: var(--fontFamily);\n  color: var(--textColor);\n  background-color: var(--backgroundColor);\n  box-sizing: border-box;\n  padding: 32px;\n  border-radius: 5px;\n  pointer-events: auto;\n  border: 0px;\n  position: fixed;\n  bottom: 16px;\n  right: 16px;\n  width: 600px;\n  overflow: auto;\n  max-width: calc(100% - 32px);\n  max-height: calc(100vh - 32px);\n  transform: translate(0, -20px);\n  opacity: 0;\n  animation: silktide-slideInDown 350ms ease-out forwards;\n  animation-delay: 0.3s;\n  box-shadow: -5px 5px 10px 0px #00000012, 0px 0px 50px 0px #0000001a;\n}\n\n#silktide-banner:focus {\n  border-radius: 50%;\n}\n\n#silktide-banner.center {\n  top: 50%;\n  left: 50%;\n  bottom: auto;\n  right: auto;\n  position: fixed;\n  transform: translate(-50%, calc(-50% - 20px));\n  animation: silktide-slideInDown-center 350ms ease-out forwards;\n}\n\n#silktide-banner.bottomLeft {\n  bottom: 16px;\n  left: 16px;\n  position: fixed;\n}\n\n#silktide-banner.bottomCenter {\n  bottom: 16px;\n  left: 50%;\n  position: fixed;\n  transform: translate(-50%, -20px);\n  animation: silktide-slideInDown-bottomCenter 350ms ease-out forwards;\n}\n\n#silktide-banner .preferences {\n  display: flex;\n  gap: 5px;\n  border: none;\n  padding: 15px 0px;\n  background-color: transparent;\n  color: var(--primaryColor);\n  cursor: pointer;\n  font-size: 16px;\n}\n\n#silktide-banner .preferences span {\n  display: block;\n  white-space: nowrap;\n  text-decoration: underline;\n}\n\n#silktide-banner .preferences span:hover {\n  color: var(--textColor);\n}\n\n#silktide-banner .preferences:after {\n  display: block;\n  content: '>';\n  text-decoration: none;\n}\n\n#silktide-banner p {\n  font-size: 16px;\n  line-height: 24px;\n  margin: 0px 0px 15px;\n}\n\n#silktide-banner a {\n  display: inline-block;\n  color: var(--primaryColor);\n  text-decoration: underline;\n  background-color: var(--backgroundColor);\n}\n\n#silktide-banner a:hover {\n  color: var(--textColor);\n}\n\n#silktide-banner a.silktide-logo {\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  fill: var(--primaryColor); /* passed down to svg > path */\n  margin-left: auto;\n  width: 48px;\n  height: 48px;\n}\n\n\n#silktide-banner .actions {\n  display: flex;\n  gap: 16px;\n  flex-direction: column;\n  margin-top: 24px;\n}\n\n@media (min-width: 600px) {\n  #silktide-banner .actions {\n    flex-direction: row;\n    align-items: center;\n  }\n}\n\n#silktide-banner .actions-row {\n  display: flex;\n  gap: 16px;\n  flex-direction: row;\n  align-items: center;\n  justify-content: space-between;\n  flex-grow: 1;\n}\n\n/* --------------------------------\n  Modal\n-------------------------------- */\n#silktide-modal {\n  display: none;\n  pointer-events: auto;\n  overflow: auto;\n  width: 800px;\n  max-width: 100%;\n  max-height: 100%;\n  border: 0px;\n  transform: translate(0px, -20px);\n  opacity: 0;\n  animation: silktide-slideInUp-center 350ms ease-out forwards;\n  box-shadow: -5px 5px 10px 0px #00000012, 0px 0px 50px 0px #0000001a;\n  font-family: var(--fontFamily);\n  color: var(--textColor);\n  flex-direction: column;\n  padding: 30px;\n  background-color: var(--backgroundColor);\n  border-radius: 5px;\n  box-sizing: border-box;\n}\n\n/* --------------------------------\n  Modal - Header\n-------------------------------- */\n#silktide-modal header {\n  display: flex;\n  justify-content: space-between;\n  align-items: center;\n  margin-bottom: 20px;\n  gap: 16px;\n}\n\n#silktide-modal h1 {\n  font-family: var(--fontFamily);\n  color: var(--textColor);\n  font-size: 24px;\n  font-weight: 500;\n  margin: 0px;\n}\n\n#silktide-modal .modal-close {\n  display: inline-flex;\n  border: none;\n  padding: 13px;\n  border: 0px;\n  cursor: pointer;\n  background: var(--backgroundColor);\n  color: var(--primaryColor);\n}\n\n#silktide-modal .modal-close svg {\n  fill: var(--primaryColor);\n}\n\n/* --------------------------------\n  Modal - Content\n-------------------------------- */\n\n#silktide-modal section {\n  flex: 1;\n  margin-top: 32px;\n}\n\n#silktide-modal section::-webkit-scrollbar {\n  display: block; /* Force scrollbars to show */\n  width: 5px; /* Width of the scrollbar */\n}\n\n#silktide-modal section::-webkit-scrollbar-thumb {\n  background-color: var(--textColor); /* Color of the scrollbar thumb */\n  border-radius: 10px; /* Rounded corners for the thumb */\n}\n\n#silktide-modal p {\n  font-size: 16px;\n  line-height: 24px;\n  color: var(--textColor);\n  margin: 0px 0px 15px;\n}\n\n#silktide-modal p:last-of-type {\n  margin: 0px;\n}\n\n#silktide-modal fieldset {\n  padding: 0px;\n  border: none;\n  margin: 0px 0px 32px;\n}\n\n#silktide-modal fieldset:last-of-type {\n  margin: 0px;\n}\n\n#silktide-modal legend {\n  padding: 0px;\n  margin: 0px 0px 10px;\n  font-weight: 700;\n  color: var(--textColor);\n  font-size: 16px;\n}\n\n#silktide-modal .cookie-type-content {\n  display: flex;\n  justify-content: space-between;\n  align-items: flex-start;\n  gap: 24px;  \n}\n\n/* --------------------------------\n  Modal - Switches\n-------------------------------- */\n#silktide-modal .switch {\n  flex-shrink: 0;\n  position: relative;\n  display: inline-block;\n  height: 34px;\n  width: 74px;\n  cursor: pointer;\n}\n\n#silktide-modal .switch:focus-within {\n  outline: none;\n  box-shadow: var(--focus);\n  border-radius: 25px;\n}\n\n#silktide-modal .switch input {\n  opacity: 0;\n  position: absolute;\n}\n\n/* Unchecked Switch Styles */\n#silktide-modal .switch__pill {\n  position: relative;\n  display: block;\n  height: 34px;\n  width: 74px;\n  background: var(--textColor);\n  border-radius: 25px;\n}\n\n#silktide-modal .switch__dot {\n  position: absolute;\n  top: 2px;\n  left: 2px;\n  display: block;\n  height: 30px;\n  width: 30px;\n  background: var(--backgroundColor);\n  border-radius: 50%;\n  transition: left 150ms ease-out;\n}\n\n#silktide-modal .switch__off,\n#silktide-modal .switch__on {\n  text-transform: uppercase;\n  font-size: 15px;\n  font-weight: 500;\n  color: var(--backgroundColor);\n  position: absolute;\n  top: 7px;\n  right: 8px;\n  transition: right 150ms ease-out, opacity 150ms ease-out;\n}\n\n#silktide-modal .switch__off {\n  opacity: 1;\n}\n\n#silktide-modal .switch__on {\n  opacity: 0;\n}\n\n/* Checked Switch Styles */\n#silktide-modal .switch input:checked + .switch__pill {\n  background: var(--primaryColor);\n}\n\n#silktide-modal .switch input:checked ~ .switch__dot {\n  left: calc(100% - 32px);\n}\n\n#silktide-modal .switch input:checked ~ .switch__off {\n  right: calc(100% - 32px);\n  opacity: 0;\n}\n\n#silktide-modal .switch input:checked ~ .switch__on {\n  right: calc(100% - 34px);\n  opacity: 1;\n}\n\n/* Disabled Switch Styles */\n#silktide-modal .switch input:disabled + .switch__pill {\n  opacity: 0.65;\n  cursor: not-allowed;\n}\n\n/* --------------------------------\n  Modal - Footer\n-------------------------------- */\n#silktide-modal footer {\n  display: flex;\n  flex-direction: column;\n  gap: 16px;\n  margin-top: 24px;\n}\n\n@media (min-width: 600px) {\n  #silktide-modal footer {\n    flex-direction: row;\n    align-items: center;\n  }\n}\n\n#silktide-modal footer a {\n  margin-left: auto;\n  padding: 14px 0px;\n}\n\n/* Cookie Icon */\n#silktide-cookie-icon {\n  display: none;\n  position: fixed;\n  bottom: 10px;\n  left: 10px;\n  justify-content: center;\n  align-items: center;\n  width: 60px;\n  height: 60px;\n  border-radius: 50%;\n  padding: 0px;\n  border: none;\n  background-color: var(--cookieIconColor);\n  cursor: pointer;\n  box-shadow: 0px 0px 6px 0px #0000001a;\n  pointer-events: auto;\n  animation: silktide-fadeIn 0.3s ease-in-out forwards;\n}\n\n#silktide-cookie-icon.bottomRight {\n  left: auto;\n  right: 10px;\n}\n\n#silktide-cookie-icon svg {\n  fill: var(--cookieIconBackgroundColor);\n}\n\n/* --------------------------------\n  Backdrop\n-------------------------------- */\n#silktide-backdrop {\n  display: none;\n  position: absolute;\n  top: 0;\n  left: 0;\n  width: 100%;\n  height: 100%;\n  background-color: var(--backdropBackgroundColor);\n  backdrop-filter: blur(var(--backdropBackgroundBlur));\n  pointer-events: all;\n}\n\n/* --------------------------------\n  Animations\n-------------------------------- */\n@keyframes silktide-fadeIn {\n  from {\n    opacity: 0;\n  }\n  to {\n    opacity: 1;\n  }\n}\n\n@keyframes silktide-slideInDown {\n  from {\n    opacity: 0;\n    transform: translateY(-20px);\n  }\n  to {\n    opacity: 1;\n    transform: translateY(0);\n  }\n}\n\n@keyframes silktide-slideInDown-center {\n  from {\n    opacity: 0;\n    transform: translate(-50%, calc(-50% - 20px));\n  }\n  to {\n    opacity: 1;\n    transform: translate(-50%, -50%);\n  }\n}\n\n@keyframes silktide-slideInDown-bottomCenter {\n  from {\n    opacity: 0;\n    transform: translate(-50%, -20px);\n  }\n  to {\n    opacity: 1;\n    transform: translate(-50%, 0);\n  }\n}\n\n@keyframes silktide-slideInUp-center {\n  from {\n    opacity: 0;\n    transform: translate(0px, 20px);\n  }\n  to {\n    opacity: 1;\n    transform: translate(0px, 0px);\n  }\n}\n"
  },
  {
    "path": "docs/_static/silktide-consent-manager.js",
    "content": "// Silktide Consent Manager - https://silktide.com/consent-manager/  \n\nclass SilktideCookieBanner {\n  constructor(config) {\n    this.config = config; // Save config to the instance\n\n    this.wrapper = null;\n    this.banner = null;\n    this.modal = null;\n    this.cookieIcon = null;\n    this.backdrop = null;\n\n    this.createWrapper();\n\n    if (this.shouldShowBackdrop()) {\n      this.createBackdrop();\n    }\n\n    this.createCookieIcon();\n    this.createModal();\n\n    if (this.shouldShowBanner()) {\n      this.createBanner();\n      this.showBackdrop();\n    } else {\n      this.showCookieIcon();\n    }\n\n    this.setupEventListeners();\n\n    if (this.hasSetInitialCookieChoices()) {\n      this.loadRequiredCookies();\n      this.runAcceptedCookieCallbacks();\n    }\n  }\n\n  destroyCookieBanner() {\n    // Remove all cookie banner elements from the DOM\n    if (this.wrapper && this.wrapper.parentNode) {\n      this.wrapper.parentNode.removeChild(this.wrapper);\n    }\n\n    // Restore scrolling\n    this.allowBodyScroll();\n\n    // Clear all references\n    this.wrapper = null;\n    this.banner = null;\n    this.modal = null;\n    this.cookieIcon = null;\n    this.backdrop = null;\n  }\n\n  // ----------------------------------------------------------------\n  // Wrapper\n  // ----------------------------------------------------------------\n  createWrapper() {\n    this.wrapper = document.createElement('div');\n    this.wrapper.id = 'silktide-wrapper';\n    document.body.insertBefore(this.wrapper, document.body.firstChild);\n  }\n\n  // ----------------------------------------------------------------\n  // Wrapper Child Generator\n  // ----------------------------------------------------------------\n  createWrapperChild(htmlContent, id) {\n    // Create child element\n    const child = document.createElement('div');\n    child.id = id;\n    child.innerHTML = htmlContent;\n\n    // Ensure wrapper exists\n    if (!this.wrapper || !document.body.contains(this.wrapper)) {\n      this.createWrapper();\n    }\n\n    // Append child to wrapper\n    this.wrapper.appendChild(child);\n    return child;\n  }\n\n  // ----------------------------------------------------------------\n  // Backdrop\n  // ----------------------------------------------------------------\n  createBackdrop() {\n    this.backdrop = this.createWrapperChild(null, 'silktide-backdrop');\n  }\n\n  showBackdrop() {\n    if (this.backdrop) {\n      this.backdrop.style.display = 'block';\n    }\n    // Trigger optional onBackdropOpen callback\n    if (typeof this.config.onBackdropOpen === 'function') {\n      this.config.onBackdropOpen();\n    }\n  }\n\n  hideBackdrop() {\n    if (this.backdrop) {\n      this.backdrop.style.display = 'none';\n    }\n\n    // Trigger optional onBackdropClose callback\n    if (typeof this.config.onBackdropClose === 'function') {\n      this.config.onBackdropClose();\n    }\n  }\n\n  shouldShowBackdrop() {\n    return this.config?.background?.showBackground || false;\n  }\n\n  // update the checkboxes in the modal with the values from localStorage\n  updateCheckboxState(saveToStorage = false) {\n    const preferencesSection = this.modal.querySelector('#cookie-preferences');\n    const checkboxes = preferencesSection.querySelectorAll('input[type=\"checkbox\"]');\n\n    checkboxes.forEach((checkbox) => {\n      const [, cookieId] = checkbox.id.split('cookies-');\n      const cookieType = this.config.cookieTypes.find(type => type.id === cookieId);\n      \n      if (!cookieType) return;\n\n      if (saveToStorage) {\n        // Save the current state to localStorage and run callbacks\n        const currentState = checkbox.checked;\n        \n        if (cookieType.required) {\n          localStorage.setItem(\n            `silktideCookieChoice_${cookieId}${this.getBannerSuffix()}`,\n            'true'\n          );\n        } else {\n          localStorage.setItem(\n            `silktideCookieChoice_${cookieId}${this.getBannerSuffix()}`,\n            currentState.toString()\n          );\n          \n          // Run appropriate callback\n          if (currentState && typeof cookieType.onAccept === 'function') {\n            cookieType.onAccept();\n          } else if (!currentState && typeof cookieType.onReject === 'function') {\n            cookieType.onReject();\n          }\n        }\n      } else {\n        // When reading values (opening modal)\n        if (cookieType.required) {\n          checkbox.checked = true;\n          checkbox.disabled = true;\n        } else {\n          const storedValue = localStorage.getItem(\n            `silktideCookieChoice_${cookieId}${this.getBannerSuffix()}`\n          );\n          \n          if (storedValue !== null) {\n            checkbox.checked = storedValue === 'true';\n          } else {\n            checkbox.checked = !!cookieType.defaultValue;\n          }\n        }\n      }\n    });\n  }\n\n  setInitialCookieChoiceMade() {\n    window.localStorage.setItem(`silktideCookieBanner_InitialChoice${this.getBannerSuffix()}`, 1);\n  }\n\n  // ----------------------------------------------------------------\n  // Consent Handling\n  // ----------------------------------------------------------------\n  handleCookieChoice(accepted) {\n    // We set that an initial choice was made regardless of what it was so we don't show the banner again\n    this.setInitialCookieChoiceMade();\n\n    this.removeBanner();\n    this.hideBackdrop();\n    this.toggleModal(false);\n    this.showCookieIcon();\n\n    this.config.cookieTypes.forEach((type) => {\n      // Set localStorage and run accept/reject callbacks\n      if (type.required == true) {\n        localStorage.setItem(`silktideCookieChoice_${type.id}${this.getBannerSuffix()}`, 'true');\n        if (typeof type.onAccept === 'function') { type.onAccept() }\n      } else {\n        localStorage.setItem(\n          `silktideCookieChoice_${type.id}${this.getBannerSuffix()}`,\n          accepted.toString(),\n        );\n\n        if (accepted) {\n          if (typeof type.onAccept === 'function') { type.onAccept(); }\n        } else {\n          if (typeof type.onReject === 'function') { type.onReject(); }\n        }\n      }\n    });\n\n    // Trigger optional onAcceptAll/onRejectAll callbacks\n    if (accepted && typeof this.config.onAcceptAll === 'function') {\n      if (typeof this.config.onAcceptAll === 'function') { this.config.onAcceptAll(); }\n    } else if (typeof this.config.onRejectAll === 'function') {\n      if (typeof this.config.onRejectAll === 'function') { this.config.onRejectAll(); }\n    }\n\n    // finally update the checkboxes in the modal with the values from localStorage\n    this.updateCheckboxState();\n  }\n\n  getAcceptedCookies() {\n    return (this.config.cookieTypes || []).reduce((acc, cookieType) => {\n      acc[cookieType.id] =\n        localStorage.getItem(`silktideCookieChoice_${cookieType.id}${this.getBannerSuffix()}`) ===\n        'true';\n      return acc;\n    }, {});\n  }\n\n  runAcceptedCookieCallbacks() {\n    if (!this.config.cookieTypes) return;\n\n    const acceptedCookies = this.getAcceptedCookies();\n    this.config.cookieTypes.forEach((type) => {\n      if (type.required) return; // we run required cookies separately in loadRequiredCookies\n      if (acceptedCookies[type.id] && typeof type.onAccept === 'function') {\n        if (typeof type.onAccept === 'function') { type.onAccept(); }\n      }\n    });\n  }\n\n  runRejectedCookieCallbacks() {\n    if (!this.config.cookieTypes) return;\n\n    const rejectedCookies = this.getRejectedCookies();\n    this.config.cookieTypes.forEach((type) => {\n      if (rejectedCookies[type.id] && typeof type.onReject === 'function') {\n        if (typeof type.onReject === 'function') { type.onReject(); }\n      }\n    });\n  }\n\n  /**\n   * Run through all of the cookie callbacks based on the current localStorage values\n   */\n  runStoredCookiePreferenceCallbacks() {\n    this.config.cookieTypes.forEach((type) => {\n      const accepted =\n        localStorage.getItem(`silktideCookieChoice_${type.id}${this.getBannerSuffix()}`) === 'true';\n      // Set localStorage and run accept/reject callbacks\n      if (accepted) {\n        if (typeof type.onAccept === 'function') { type.onAccept(); }\n      } else {\n        if (typeof type.onReject === 'function') { type.onReject(); }\n      }\n    });\n  }\n\n  loadRequiredCookies() {\n    if (!this.config.cookieTypes) return;\n    this.config.cookieTypes.forEach((cookie) => {\n      if (cookie.required && typeof cookie.onAccept === 'function') {\n        if (typeof cookie.onAccept === 'function') { cookie.onAccept(); }\n      }\n    });\n  }\n\n  // ----------------------------------------------------------------\n  // Banner\n  // ----------------------------------------------------------------\n  getBannerContent() {\n    const bannerDescription =\n      this.config.text?.banner?.description ||\n      \"<p>We use cookies on our site to enhance your user experience, provide personalized content, and analyze our traffic.</p>\";\n\n    // Accept button\n    const acceptAllButtonText = this.config.text?.banner?.acceptAllButtonText || 'Accept all';\n    const acceptAllButtonLabel = this.config.text?.banner?.acceptAllButtonAccessibleLabel;\n    const acceptAllButton = `<button class=\"accept-all st-button st-button--primary\"${\n      acceptAllButtonLabel && acceptAllButtonLabel !== acceptAllButtonText \n        ? ` aria-label=\"${acceptAllButtonLabel}\"` \n        : ''\n    }>${acceptAllButtonText}</button>`;\n    \n    // Reject button\n    const rejectNonEssentialButtonText = this.config.text?.banner?.rejectNonEssentialButtonText || 'Reject non-essential';\n    const rejectNonEssentialButtonLabel = this.config.text?.banner?.rejectNonEssentialButtonAccessibleLabel;\n    const rejectNonEssentialButton = `<button class=\"reject-all st-button st-button--primary\"${\n      rejectNonEssentialButtonLabel && rejectNonEssentialButtonLabel !== rejectNonEssentialButtonText \n        ? ` aria-label=\"${rejectNonEssentialButtonLabel}\"` \n        : ''\n    }>${rejectNonEssentialButtonText}</button>`;\n\n    // Preferences button\n    const preferencesButtonText = this.config.text?.banner?.preferencesButtonText || 'Preferences';\n    const preferencesButtonLabel = this.config.text?.banner?.preferencesButtonAccessibleLabel;\n    const preferencesButton = `<button class=\"preferences\"${\n      preferencesButtonLabel && preferencesButtonLabel !== preferencesButtonText \n        ? ` aria-label=\"${preferencesButtonLabel}\"` \n        : ''\n    }><span>${preferencesButtonText}</span></button>`;\n    \n\n    // Silktide logo link\n    const silktideLogo = `\n      <a class=\"silktide-logo\" href=\"https://silktide.com/consent-manager\" target=\"_blank\" rel=\"noreferrer\" aria-label=\"Visit the Silktide Consent Manager page\">\n        <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"25\" viewBox=\"0 0 24 25\" fill=\"inherit\">\n          <path fill-rule=\"evenodd\" clip-rule=\"evenodd\" d=\"M14.1096 16.7745C13.8895 17.2055 13.3537 17.3805 12.9129 17.1653L8.28443 14.9055L2.73192 17.7651L11.1025 21.9814C11.909 22.3876 12.8725 22.3591 13.6524 21.9058L20.4345 17.9645C21.2845 17.4704 21.7797 16.5522 21.7164 15.5872L21.7088 15.4704C21.6487 14.5561 21.0962 13.7419 20.2579 13.3326L15.6793 11.0972L10.2283 13.9045L13.71 15.6043C14.1507 15.8195 14.3297 16.3434 14.1096 16.7745ZM8.2627 12.9448L13.7136 10.1375L10.2889 8.46543C9.84803 8.25021 9.66911 7.72629 9.88916 7.29524C10.1093 6.86417 10.6451 6.68921 11.0859 6.90442L15.6575 9.13647L21.2171 6.27325L12.8808 2.03496C12.0675 1.62147 11.0928 1.65154 10.3078 2.11432L3.54908 6.09869C2.70732 6.59492 2.21846 7.50845 2.28139 8.46761L2.29003 8.59923C2.35002 9.51362 2.9026 10.3278 3.7409 10.7371L8.2627 12.9448ZM6.31884 13.9458L2.94386 12.2981C1.53727 11.6113 0.610092 10.2451 0.509431 8.71094L0.500795 8.57933C0.3952 6.96993 1.21547 5.4371 2.62787 4.60447L9.38662 0.620092C10.7038 -0.156419 12.3392 -0.206861 13.7039 0.486938L23.3799 5.40639C23.4551 5.44459 23.5224 5.4918 23.5811 5.54596C23.7105 5.62499 23.8209 5.73754 23.897 5.87906C24.1266 6.30534 23.9594 6.83293 23.5234 7.05744L17.6231 10.0961L21.0549 11.7716C22.4615 12.4583 23.3887 13.8245 23.4893 15.3587L23.497 15.4755C23.6033 17.0947 22.7724 18.6354 21.346 19.4644L14.5639 23.4057C13.2554 24.1661 11.6386 24.214 10.2854 23.5324L0.621855 18.6649C0.477299 18.592 0.361696 18.4859 0.279794 18.361C0.210188 18.2968 0.150054 18.2204 0.10296 18.133C-0.126635 17.7067 0.0406445 17.1792 0.47659 16.9546L6.31884 13.9458Z\" fill=\"inherit\"/>\n        </svg>\n      </a>\n    `;\n\n    const bannerContent = `\n      ${bannerDescription}\n      <div class=\"actions\">                               \n        ${acceptAllButton}\n        ${rejectNonEssentialButton}\n        <div class=\"actions-row\">\n          ${preferencesButton}\n          ${silktideLogo}\n        </div>\n      </div>\n    `;\n\n    return bannerContent;\n  }\n\n  hasSetInitialCookieChoices() {\n    return !!localStorage.getItem(`silktideCookieBanner_InitialChoice${this.getBannerSuffix()}`);\n  }\n\n  createBanner() {\n    // Create banner element\n    this.banner = this.createWrapperChild(this.getBannerContent(), 'silktide-banner');\n\n    // Add positioning class from config\n    if (this.banner && this.config.position?.banner) {\n      this.banner.classList.add(this.config.position.banner);\n    }\n\n    // Trigger optional onBannerOpen callback\n    if (this.banner && typeof this.config.onBannerOpen === 'function') {\n      this.config.onBannerOpen();\n    }\n  }\n\n  removeBanner() {\n    if (this.banner && this.banner.parentNode) {\n      this.banner.parentNode.removeChild(this.banner);\n      this.banner = null;\n\n      // Trigger optional onBannerClose callback\n      if (typeof this.config.onBannerClose === 'function') {\n        this.config.onBannerClose();\n      }\n    }\n  }\n\n  shouldShowBanner() {\n    if (this.config.showBanner === false) {\n      return false;\n    }\n    return (\n      localStorage.getItem(`silktideCookieBanner_InitialChoice${this.getBannerSuffix()}`) === null\n    );\n  }\n\n  // ----------------------------------------------------------------\n  // Modal\n  // ----------------------------------------------------------------\n  getModalContent() {\n    const preferencesTitle =\n      this.config.text?.preferences?.title || 'Customize your cookie preferences';\n    \n    const preferencesDescription =\n      this.config.text?.preferences?.description ||\n      \"<p>We respect your right to privacy. You can choose not to allow some types of cookies. Your cookie preferences will apply across our website.</p>\";\n    \n    // Preferences button\n    const preferencesButtonLabel = this.config.text?.banner?.preferencesButtonAccessibleLabel;\n\n    const closeModalButton = `<button class=\"modal-close\"${preferencesButtonLabel ? ` aria-label=\"${preferencesButtonLabel}\"` : ''}>\n      <svg width=\"20\" height=\"20\" viewBox=\"0 0 20 20\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\">\n          <path d=\"M19.4081 3.41559C20.189 2.6347 20.189 1.36655 19.4081 0.585663C18.6272 -0.195221 17.3591 -0.195221 16.5782 0.585663L10 7.17008L3.41559 0.59191C2.6347 -0.188974 1.36655 -0.188974 0.585663 0.59191C-0.195221 1.37279 -0.195221 2.64095 0.585663 3.42183L7.17008 10L0.59191 16.5844C-0.188974 17.3653 -0.188974 18.6335 0.59191 19.4143C1.37279 20.1952 2.64095 20.1952 3.42183 19.4143L10 12.8299L16.5844 19.4081C17.3653 20.189 18.6335 20.189 19.4143 19.4081C20.1952 18.6272 20.1952 17.3591 19.4143 16.5782L12.8299 10L19.4081 3.41559Z\"/>\n      </svg>\n    </button>`;\n    \n\n    const cookieTypes = this.config.cookieTypes || [];\n    const acceptedCookieMap = this.getAcceptedCookies();\n\n    // Accept button\n    const acceptAllButtonText = this.config.text?.banner?.acceptAllButtonText || 'Accept all';\n    const acceptAllButtonLabel = this.config.text?.banner?.acceptAllButtonAccessibleLabel;\n    const acceptAllButton = `<button class=\"preferences-accept-all st-button st-button--primary\"${\n      acceptAllButtonLabel && acceptAllButtonLabel !== acceptAllButtonText \n        ? ` aria-label=\"${acceptAllButtonLabel}\"` \n        : ''\n    }>${acceptAllButtonText}</button>`;\n    \n    // Reject button\n    const rejectNonEssentialButtonText = this.config.text?.banner?.rejectNonEssentialButtonText || 'Reject non-essential';\n    const rejectNonEssentialButtonLabel = this.config.text?.banner?.rejectNonEssentialButtonAccessibleLabel;\n    const rejectNonEssentialButton = `<button class=\"preferences-reject-all st-button st-button--primary\"${\n      rejectNonEssentialButtonLabel && rejectNonEssentialButtonLabel !== rejectNonEssentialButtonText \n        ? ` aria-label=\"${rejectNonEssentialButtonLabel}\"` \n        : ''\n    }>${rejectNonEssentialButtonText}</button>`;\n    \n    // Credit link\n    const creditLinkText = this.config.text?.preferences?.creditLinkText || 'Get this banner for free';\n    const creditLinkAccessibleLabel = this.config.text?.preferences?.creditLinkAccessibleLabel;\n    const creditLink = `<a href=\"https://silktide.com/consent-manager\" target=\"_blank\" rel=\"noreferrer\"${\n      creditLinkAccessibleLabel && creditLinkAccessibleLabel !== creditLinkText\n        ? ` aria-label=\"${creditLinkAccessibleLabel}\"`\n        : ''\n    }>${creditLinkText}</a>`;\n    \n    \n\n    const modalContent = `\n      <header>\n        <h1>${preferencesTitle}</h1>                    \n        ${closeModalButton}\n      </header>\n      ${preferencesDescription}\n      <section id=\"cookie-preferences\">\n        ${cookieTypes\n          .map((type) => {\n            const accepted = acceptedCookieMap[type.id];\n            let isChecked = false;\n\n            // if it's accepted then show as checked\n            if (accepted) {\n              isChecked = true;\n            }\n\n            // if nothing has been accepted / rejected yet, then show as checked if the default value is true\n            if (!accepted && !this.hasSetInitialCookieChoices()) {\n              isChecked = type.defaultValue;\n            }\n\n            return `\n            <fieldset>\n                <legend>${type.name}</legend>\n                <div class=\"cookie-type-content\">\n                    <div class=\"cookie-type-description\">${type.description}</div>\n                    <label class=\"switch\" for=\"cookies-${type.id}\">\n                        <input type=\"checkbox\" id=\"cookies-${type.id}\" ${\n              type.required ? 'checked disabled' : isChecked ? 'checked' : ''\n            } />\n                        <span class=\"switch__pill\" aria-hidden=\"true\"></span>\n                        <span class=\"switch__dot\" aria-hidden=\"true\"></span>\n                        <span class=\"switch__off\" aria-hidden=\"true\">Off</span>\n                        <span class=\"switch__on\" aria-hidden=\"true\">On</span>\n                    </label>\n                </div>\n            </fieldset>\n        `;\n          })\n          .join('')}\n      </section>\n      <footer>\n        ${acceptAllButton}\n        ${rejectNonEssentialButton}\n        ${creditLink}\n      </footer>\n    `;\n\n    return modalContent;\n  }\n\n  createModal() {\n    // Create banner element\n    this.modal = this.createWrapperChild(this.getModalContent(), 'silktide-modal');\n  }\n\n  toggleModal(show) {\n    if (!this.modal) return;\n\n    this.modal.style.display = show ? 'flex' : 'none';\n\n    if (show) {\n      this.showBackdrop();\n      this.hideCookieIcon();\n      this.removeBanner();\n      this.preventBodyScroll();\n\n      // Focus the close button\n      const modalCloseButton = this.modal.querySelector('.modal-close');\n      modalCloseButton.focus();\n\n      // Trigger optional onPreferencesOpen callback\n      if (typeof this.config.onPreferencesOpen === 'function') {\n        this.config.onPreferencesOpen();\n      }\n\n      this.updateCheckboxState(false); // read from storage when opening\n    } else {\n      // Set that an initial choice was made when closing the modal\n      this.setInitialCookieChoiceMade();\n      \n      // Save current checkbox states to storage\n      this.updateCheckboxState(true);\n\n      this.hideBackdrop();\n      this.showCookieIcon();\n      this.allowBodyScroll();\n\n      // Trigger optional onPreferencesClose callback\n      if (typeof this.config.onPreferencesClose === 'function') {\n        this.config.onPreferencesClose();\n      }\n    }\n  }\n\n  // ----------------------------------------------------------------\n  // Cookie Icon\n  // ----------------------------------------------------------------\n  getCookieIconContent() {\n    return `\n      <svg width=\"38\" height=\"38\" viewBox=\"0 0 38 38\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\">\n          <path d=\"M19.1172 1.15625C19.0547 0.734374 18.7344 0.390624 18.3125 0.328124C16.5859 0.0859365 14.8281 0.398437 13.2813 1.21875L7.5 4.30469C5.96094 5.125 4.71875 6.41406 3.95313 7.98437L1.08594 13.8906C0.320314 15.4609 0.0703136 17.2422 0.375001 18.9609L1.50781 25.4297C1.8125 27.1562 2.64844 28.7344 3.90625 29.9531L8.61719 34.5156C9.875 35.7344 11.4766 36.5156 13.2031 36.7578L19.6875 37.6719C21.4141 37.9141 23.1719 37.6016 24.7188 36.7812L30.5 33.6953C32.0391 32.875 33.2813 31.5859 34.0469 30.0078L36.9141 24.1094C37.6797 22.5391 37.9297 20.7578 37.625 19.0391C37.5547 18.625 37.2109 18.3125 36.7969 18.25C32.7734 17.6094 29.5469 14.5703 28.6328 10.6406C28.4922 10.0469 28.0078 9.59375 27.4063 9.5C23.1406 8.82031 19.7734 5.4375 19.1094 1.15625H19.1172ZM15.25 10.25C15.913 10.25 16.5489 10.5134 17.0178 10.9822C17.4866 11.4511 17.75 12.087 17.75 12.75C17.75 13.413 17.4866 14.0489 17.0178 14.5178C16.5489 14.9866 15.913 15.25 15.25 15.25C14.587 15.25 13.9511 14.9866 13.4822 14.5178C13.0134 14.0489 12.75 13.413 12.75 12.75C12.75 12.087 13.0134 11.4511 13.4822 10.9822C13.9511 10.5134 14.587 10.25 15.25 10.25ZM10.25 25.25C10.25 24.587 10.5134 23.9511 10.9822 23.4822C11.4511 23.0134 12.087 22.75 12.75 22.75C13.413 22.75 14.0489 23.0134 14.5178 23.4822C14.9866 23.9511 15.25 24.587 15.25 25.25C15.25 25.913 14.9866 26.5489 14.5178 27.0178C14.0489 27.4866 13.413 27.75 12.75 27.75C12.087 27.75 11.4511 27.4866 10.9822 27.0178C10.5134 26.5489 10.25 25.913 10.25 25.25ZM27.75 20.25C28.413 20.25 29.0489 20.5134 29.5178 20.9822C29.9866 21.4511 30.25 22.087 30.25 22.75C30.25 23.413 29.9866 24.0489 29.5178 24.5178C29.0489 24.9866 28.413 25.25 27.75 25.25C27.087 25.25 26.4511 24.9866 25.9822 24.5178C25.5134 24.0489 25.25 23.413 25.25 22.75C25.25 22.087 25.5134 21.4511 25.9822 20.9822C26.4511 20.5134 27.087 20.25 27.75 20.25Z\" />\n      </svg>\n    `;\n  }\n\n  createCookieIcon() {\n    this.cookieIcon = document.createElement('button');\n    this.cookieIcon.id = 'silktide-cookie-icon';\n    this.cookieIcon.title = 'Manage your cookie preferences for this site';\n    this.cookieIcon.innerHTML = this.getCookieIconContent();\n\n    if (this.config.text?.banner?.preferencesButtonAccessibleLabel) {\n      this.cookieIcon.ariaLabel = this.config.text?.banner?.preferencesButtonAccessibleLabel;\n    }\n\n    // Ensure wrapper exists\n    if (!this.wrapper || !document.body.contains(this.wrapper)) {\n      this.createWrapper();\n    }\n\n    // Append child to wrapper\n    this.wrapper.appendChild(this.cookieIcon);\n\n    // Add positioning class from config\n    if (this.cookieIcon && this.config.cookieIcon?.position) {\n      this.cookieIcon.classList.add(this.config.cookieIcon.position);\n    }\n\n    // Add color scheme class from config\n    if (this.cookieIcon && this.config.cookieIcon?.colorScheme) {\n      this.cookieIcon.classList.add(this.config.cookieIcon.colorScheme);\n    }\n  }\n\n  showCookieIcon() {\n    if (this.cookieIcon) {\n      this.cookieIcon.style.display = 'flex';\n    }\n  }\n\n  hideCookieIcon() {\n    if (this.cookieIcon) {\n      this.cookieIcon.style.display = 'none';\n    }\n  }\n\n  /**\n   * This runs if the user closes the modal without making a choice for the first time\n   * We apply the default values and the necessary values as default\n   */\n  handleClosedWithNoChoice() {\n    this.config.cookieTypes.forEach((type) => {\n      let accepted = true;\n      // Set localStorage and run accept/reject callbacks\n      if (type.required == true || type.defaultValue) {\n        localStorage.setItem(\n          `silktideCookieChoice_${type.id}${this.getBannerSuffix()}`,\n          accepted.toString(),\n        );\n      } else {\n        accepted = false;\n        localStorage.setItem(\n          `silktideCookieChoice_${type.id}${this.getBannerSuffix()}`,\n          accepted.toString(),\n        );\n      }\n\n      if (accepted) {\n        if (typeof type.onAccept === 'function') { type.onAccept(); }\n      } else {\n        if (typeof type.onReject === 'function') { type.onReject(); }\n      }\n      // set the flag to say that the cookie choice has been made\n      this.setInitialCookieChoiceMade();\n      this.updateCheckboxState();\n    });\n  }\n\n  // ----------------------------------------------------------------\n  // Focusable Elements\n  // ----------------------------------------------------------------\n  getFocusableElements(element) {\n    return element.querySelectorAll(\n      'button, a[href], input, select, textarea, [tabindex]:not([tabindex=\"-1\"])',\n    );\n  }\n\n  // ----------------------------------------------------------------\n  // Event Listeners\n  // ----------------------------------------------------------------\n  setupEventListeners() {\n    // Check Banner exists before trying to add event listeners\n    if (this.banner) {\n      // Get the buttons\n      const acceptButton = this.banner.querySelector('.accept-all');\n      const rejectButton = this.banner.querySelector('.reject-all');\n      const preferencesButton = this.banner.querySelector('.preferences');\n\n      // Add event listeners to the buttons\n      acceptButton?.addEventListener('click', () => this.handleCookieChoice(true));\n      rejectButton?.addEventListener('click', () => this.handleCookieChoice(false));\n      preferencesButton?.addEventListener('click', () => {\n        this.showBackdrop();\n        this.toggleModal(true);\n      });\n\n      // Focus Trap\n      const focusableElements = this.getFocusableElements(this.banner);\n      const firstFocusableEl = focusableElements[0];\n      const lastFocusableEl = focusableElements[focusableElements.length - 1];\n\n      // Add keydown event listener to handle tab navigation\n      this.banner.addEventListener('keydown', (e) => {\n        if (e.key === 'Tab') {\n          if (e.shiftKey) {\n            if (document.activeElement === firstFocusableEl) {\n              lastFocusableEl.focus();\n              e.preventDefault();\n            }\n          } else {\n            if (document.activeElement === lastFocusableEl) {\n              firstFocusableEl.focus();\n              e.preventDefault();\n            }\n          }\n        }\n      });\n\n      // Set initial focus\n      if (this.config.mode !== 'wizard') {\n        acceptButton?.focus();\n      }\n    }\n\n    // Check Modal exists before trying to add event listeners\n    if (this.modal) {\n      const closeButton = this.modal.querySelector('.modal-close');\n      const acceptAllButton = this.modal.querySelector('.preferences-accept-all');\n      const rejectAllButton = this.modal.querySelector('.preferences-reject-all');\n\n      closeButton?.addEventListener('click', () => {\n        this.toggleModal(false);\n\n        const hasMadeFirstChoice = this.hasSetInitialCookieChoices();\n\n        if (hasMadeFirstChoice) {\n          // run through the callbacks based on the current localStorage state\n          this.runStoredCookiePreferenceCallbacks();\n        } else {\n          // handle the case where the user closes without making a choice for the first time\n          this.handleClosedWithNoChoice();\n        }\n      });\n      acceptAllButton?.addEventListener('click', () => this.handleCookieChoice(true));\n      rejectAllButton?.addEventListener('click', () => this.handleCookieChoice(false));\n\n      // Banner Focus Trap\n      const focusableElements = this.getFocusableElements(this.modal);\n      const firstFocusableEl = focusableElements[0];\n      const lastFocusableEl = focusableElements[focusableElements.length - 1];\n\n      this.modal.addEventListener('keydown', (e) => {\n        if (e.key === 'Tab') {\n          if (e.shiftKey) {\n            if (document.activeElement === firstFocusableEl) {\n              lastFocusableEl.focus();\n              e.preventDefault();\n            }\n          } else {\n            if (document.activeElement === lastFocusableEl) {\n              firstFocusableEl.focus();\n              e.preventDefault();\n            }\n          }\n        }\n        if (e.key === 'Escape') {\n          this.toggleModal(false);\n        }\n      });\n\n      closeButton?.focus();\n\n      // Update the checkbox event listeners\n      const preferencesSection = this.modal.querySelector('#cookie-preferences');\n      const checkboxes = preferencesSection.querySelectorAll('input[type=\"checkbox\"]');\n      \n      checkboxes.forEach(checkbox => {\n        checkbox.addEventListener('change', (event) => {\n          const [, cookieId] = event.target.id.split('cookies-');\n          const isAccepted = event.target.checked;\n          const previousValue = localStorage.getItem(\n            `silktideCookieChoice_${cookieId}${this.getBannerSuffix()}`\n          ) === 'true';\n          \n          // Only proceed if the value has actually changed\n          if (isAccepted !== previousValue) {\n            // Find the corresponding cookie type\n            const cookieType = this.config.cookieTypes.find(type => type.id === cookieId);\n            \n            if (cookieType) {\n              // Update localStorage\n              localStorage.setItem(\n                `silktideCookieChoice_${cookieId}${this.getBannerSuffix()}`,\n                isAccepted.toString()\n              );\n              \n              // Run the appropriate callback only if the value changed\n              if (isAccepted && typeof cookieType.onAccept === 'function') {\n                cookieType.onAccept();\n              } else if (!isAccepted && typeof cookieType.onReject === 'function') {\n                cookieType.onReject();\n              }\n            }\n          }\n        });\n      });\n    }\n\n    // Check Cookie Icon exists before trying to add event listeners\n    if (this.cookieIcon) {\n\n      this.cookieIcon.addEventListener('click', () => {\n        // If modal is not found, create it\n        if (!this.modal) {\n          this.createModal();\n          this.toggleModal(true);\n          this.hideCookieIcon();\n        }\n        // If modal is hidden, show it\n        else if (this.modal.style.display === 'none' || this.modal.style.display === '') {\n          this.toggleModal(true);\n          this.hideCookieIcon();\n        }\n        // If modal is visible, hide it\n        else {\n          this.toggleModal(false);\n        }\n      });\n    }\n  }\n\n  getBannerSuffix() {\n    if (this.config.bannerSuffix) {\n      return '_' + this.config.bannerSuffix;\n    }\n    return '';\n  }\n\n  preventBodyScroll() {\n    document.body.style.overflow = 'hidden';\n    // Prevent iOS Safari scrolling\n    document.body.style.position = 'fixed';\n    document.body.style.width = '100%';\n  }\n\n  allowBodyScroll() {\n    document.body.style.overflow = '';\n    document.body.style.position = '';\n    document.body.style.width = '';\n  }\n}\n\n(function () {\n  window.silktideCookieBannerManager = {};\n\n  let config = {};\n  let cookieBanner;\n\n  function updateCookieBannerConfig(userConfig = {}) {\n    config = {...config, ...userConfig};\n\n    // If cookie banner exists, destroy and recreate it with new config\n    if (cookieBanner) {\n      cookieBanner.destroyCookieBanner(); // We'll need to add this method\n      cookieBanner = null;\n    }\n\n    // Only initialize if document.body exists\n    if (document.body) {\n      initCookieBanner();\n    } else {\n      // Wait for DOM to be ready\n      document.addEventListener('DOMContentLoaded', initCookieBanner, {once: true});\n    }\n  }\n\n  function initCookieBanner() {\n    if (!cookieBanner) {\n      cookieBanner = new SilktideCookieBanner(config); // Pass config to the CookieBanner instance\n    }\n  }\n\n  function injectScript(url, loadOption) {\n    // Check if script with this URL already exists\n    const existingScript = document.querySelector(`script[src=\"${url}\"]`);\n    if (existingScript) {\n      return; // Script already exists, don't add it again\n    }\n\n    const script = document.createElement('script');\n    script.src = url;\n\n    // Apply the async or defer attribute based on the loadOption parameter\n    if (loadOption === 'async') {\n      script.async = true;\n    } else if (loadOption === 'defer') {\n      script.defer = true;\n    }\n\n    document.head.appendChild(script);\n  }\n\n  window.silktideCookieBannerManager.initCookieBanner = initCookieBanner;\n  window.silktideCookieBannerManager.updateCookieBannerConfig = updateCookieBannerConfig;\n  window.silktideCookieBannerManager.injectScript = injectScript;\n\n  if (document.readyState === 'loading') {\n    document.addEventListener('DOMContentLoaded', initCookieBanner, {once: true});\n  } else {\n    initCookieBanner();\n  }\n})();"
  },
  {
    "path": "docs/_static/style.css",
    "content": "/* ===================================================================\n   MOBILE-FIRST RESPONSIVE DESIGN\n   Optimized for mobile devices, tablets, and desktop screens\n   =================================================================== */\n\n/* Base Styles - Mobile First */\ntable {\n    width: 100%;\n    overflow-x: auto;\n    display: block;\n}\n\nth.head p {\n    text-align: center;\n}\n\n.table-justified td {\n    width: 50%;\n}\n\ndiv.code-block-caption {\n    padding: 4px;\n}\n\n.wy-side-nav-search {\n    background-color: #300a24;\n}\n\n.toctree-l1 {\n    padding-bottom: 3px;\n}\n\ndiv.sphinxsidebar {\n    overflow: hidden;\n}\n\ntable caption span.caption-text {\n    color: #efefef;\n    background-color: #1c4e63;\n    width: 100%;\n    display: block;\n}\n\n.banner {\n    margin: 0px -20px;\n}\n\n#searchbox {\n    margin-bottom: 15px;\n}\n\ndiv.sphinxsidebar input[name=\"q\"] {\n    border-top-left-radius: 5px;\n    border-bottom-left-radius: 5px;\n}\n\ndiv.sphinxsidebar input[type=\"submit\"] {\n    border-top-right-radius: 5px;\n    border-bottom-right-radius: 5px;\n}\n\n.help-footer {\n    margin-top: 15px;\n    border-top: 15px #320023 solid;\n}\n\n.help-footer h1, .help-footer h2 {\n    color: gray!important;\n    background-color: transparent!important;\n}\n\n.help-footer a {\n    color: #2980B9;\n    text-decoration: none;\n}\n\n.bluesky-social-icon {\n    width: 30px;\n    margin-right: 10px;\n}\n\n.inline-block {\n    display: inline-block;\n}\n\n.admonition {\n    background-color: #1c4e63;\n    background-color: #49ff8d;\n    padding: 12px;\n    border: 1px solid;\n    border-radius: 2px;\n}\n\n/* ===================================================================\n   MOBILE RESPONSIVE ENHANCEMENTS\n   =================================================================== */\n\n/* Skip to content link for accessibility */\n.skip-to-content {\n    position: absolute;\n    top: -40px;\n    left: 0;\n    background: #300a24;\n    color: #e8ffca;\n    padding: 8px 15px;\n    text-decoration: none;\n    z-index: 1002;\n    border-radius: 0 0 4px 0;\n    font-weight: 600;\n    transition: top 0.2s;\n}\n\n.skip-to-content:focus {\n    top: 0;\n    outline: 3px solid #2980B9;\n    outline-offset: 0;\n}\n\n/* Mobile Sidebar Overlay */\n.mobile-sidebar-overlay {\n    display: none;\n    position: fixed;\n    top: 0;\n    left: 0;\n    width: 100%;\n    height: 100%;\n    background-color: rgba(0, 0, 0, 0.5);\n    z-index: 998;\n    opacity: 0;\n    transition: opacity 0.3s ease-in-out;\n}\n\n.mobile-sidebar-overlay.active {\n    display: block;\n    opacity: 1;\n}\n\n/* Mobile Menu Toggle Button */\n.mobile-menu-toggle {\n    display: none;\n    position: fixed;\n    top: 15px;\n    right: 15px;\n    z-index: 1001;\n    background-color: #300a24;\n    color: #e8ffca;\n    border: 2px solid #503949;\n    padding: 10px 18px;\n    border-radius: 6px;\n    font-size: 16px;\n    font-weight: 600;\n    cursor: pointer;\n    box-shadow: 0 3px 8px rgba(0, 0, 0, 0.4);\n    transition: all 0.2s ease;\n    -webkit-tap-highlight-color: transparent;\n    user-select: none;\n}\n\n.mobile-menu-toggle:hover,\n.mobile-menu-toggle:focus {\n    background-color: #503949;\n    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.5);\n    outline: none;\n}\n\n.mobile-menu-toggle:active {\n    transform: scale(0.95);\n}\n\n/* Mobile Styles - Screens smaller than 480px (actual phones) */\n@media screen and (max-width: 480px) {\n\n    /* Show mobile menu toggle */\n    .mobile-menu-toggle {\n        display: block;\n    }\n\n    /* Document wrapper adjustments - Remove any added margins */\n    div.document {\n        width: 100% !important;\n        margin: 0 !important;\n        max-width: none !important;\n    }\n\n    div.documentwrapper {\n        float: none !important;\n        width: 100% !important;\n        margin: 0 !important;\n        margin-left: 0 !important;\n        margin-right: 0 !important;\n    }\n\n    div.bodywrapper {\n        margin: 0 !important;\n        padding: 0 !important;\n    }\n\n    div.body {\n        padding: 15px !important;\n        min-width: auto !important;\n        max-width: 100% !important;\n        margin: 0 !important;\n    }\n\n    /* Sidebar responsive behavior */\n    div.sphinxsidebar {\n        float: none !important;\n        width: 85% !important;\n        max-width: 320px !important;\n        margin: 0 !important;\n        padding: 20px !important;\n        position: fixed !important;\n        top: 0;\n        left: -100%;\n        height: 100vh;\n        overflow-y: auto;\n        overflow-x: hidden;\n        background-color: #300a24 !important;\n        z-index: 999;\n        transition: left 0.3s cubic-bezier(0.4, 0, 0.2, 1);\n        box-shadow: 3px 0 10px rgba(0, 0, 0, 0.3);\n        -webkit-overflow-scrolling: touch;\n    }\n\n    div.sphinxsidebar.mobile-open {\n        left: 0;\n    }\n\n    /* Improve sidebar content on mobile - light text on dark background */\n    div.sphinxsidebar h3 {\n        font-size: 1.1em !important;\n        margin-top: 0;\n        padding-top: 0;\n        color: #e8ffca !important;\n    }\n\n    div.sphinxsidebar ul {\n        margin-left: 0;\n        padding-left: 20px;\n    }\n\n    div.sphinxsidebar li {\n        margin: 8px 0;\n    }\n\n    div.sphinxsidebar a {\n        display: block;\n        padding: 8px 0;\n        line-height: 1.4;\n        color: #e8ffca !important;\n    }\n\n    div.sphinxsidebar a:hover {\n        color: #ffffff !important;\n        text-decoration: underline;\n    }\n\n    /* Related navigation bar */\n    div.related {\n        padding: 8px 15px !important;\n        font-size: 0.9em;\n        overflow-x: auto;\n        -webkit-overflow-scrolling: touch;\n    }\n\n    div.related ul {\n        white-space: nowrap;\n        overflow-x: auto;\n        -webkit-overflow-scrolling: touch;\n        padding-bottom: 5px;\n    }\n\n    div.related li {\n        font-size: 0.85em;\n        display: inline-block;\n    }\n\n    div.related a {\n        padding: 5px 10px;\n    }\n\n    /* Banner image */\n    .banner img {\n        max-width: 100%;\n        height: auto;\n    }\n\n    /* Typography adjustments */\n    div.body h1 {\n        font-size: 1.75em !important;\n        margin-top: 0.5em;\n        line-height: 1.2;\n        word-wrap: break-word;\n    }\n\n    div.body h2 {\n        font-size: 1.45em !important;\n        line-height: 1.3;\n        word-wrap: break-word;\n    }\n\n    div.body h3 {\n        font-size: 1.25em !important;\n        line-height: 1.3;\n    }\n\n    div.body h4,\n    div.body h5,\n    div.body h6 {\n        font-size: 1.1em !important;\n        line-height: 1.3;\n    }\n\n    div.body p,\n    div.body li {\n        line-height: 1.6;\n        word-wrap: break-word;\n        overflow-wrap: break-word;\n    }\n\n    /* Code blocks */\n    div.highlight,\n    pre {\n        overflow-x: auto !important;\n        font-size: 0.85em;\n        -webkit-overflow-scrolling: touch;\n        border-radius: 4px;\n        margin: 1em 0;\n    }\n\n    div.highlight pre {\n        white-space: pre;\n        word-wrap: normal;\n        overflow-wrap: normal;\n    }\n\n    /* Code block wrapper indicator */\n    div.highlight::after {\n        content: '← Scroll →';\n        display: block;\n        text-align: center;\n        font-size: 0.7em;\n        color: #666;\n        margin-top: -5px;\n        padding: 3px;\n        opacity: 0.6;\n    }\n\n    /* Tables */\n    table {\n        font-size: 0.85em;\n        display: block;\n        width: 100%;\n        overflow-x: auto;\n        -webkit-overflow-scrolling: touch;\n    }\n\n    table.docutils {\n        border: 0;\n        margin: 1em 0;\n    }\n\n    table.docutils td,\n    table.docutils th {\n        padding: 8px !important;\n        border: 1px solid #ccc;\n        white-space: nowrap;\n    }\n\n    table.docutils th {\n        background-color: #300a24;\n        color: #e8ffca;\n        font-weight: bold;\n    }\n\n    /* Table wrapper */\n    .table-wrapper {\n        overflow-x: auto;\n        -webkit-overflow-scrolling: touch;\n        margin: 1em 0;\n        border: 1px solid #ddd;\n        border-radius: 4px;\n    }\n\n    /* Images */\n    img {\n        max-width: 100% !important;\n        height: auto !important;\n        display: block;\n    }\n\n    /* Footer */\n    div.footer {\n        padding: 15px !important;\n        font-size: 0.9em;\n        text-align: center;\n    }\n\n    /* Search box */\n    #searchbox {\n        margin-bottom: 15px;\n    }\n\n    #searchbox input[type=\"text\"] {\n        width: 100% !important;\n        box-sizing: border-box;\n        padding: 8px;\n        font-size: 16px; /* Prevents zoom on iOS */\n    }\n\n    #searchbox input[type=\"submit\"] {\n        padding: 8px 15px;\n        font-size: 16px;\n    }\n\n    /* Admonitions */\n    .admonition {\n        padding: 12px !important;\n        margin: 15px 0 !important;\n        border-radius: 4px;\n    }\n\n    .admonition-title {\n        font-size: 1em !important;\n        font-weight: bold;\n        margin-bottom: 8px;\n    }\n\n    /* Links */\n    a {\n        word-wrap: break-word;\n        overflow-wrap: break-word;\n    }\n\n    /* Prevent text overflow */\n    div.body {\n        overflow-wrap: break-word;\n        word-wrap: break-word;\n    }\n}\n\n/* Small to Medium Screens - Restore Sphinx default layout */\n@media screen and (min-width: 481px) {\n    /* Reset document to use flex layout (Sphinx classic theme default) */\n    div.document {\n        display: flex !important;\n    }\n\n    /* Completely reset all mobile sidebar overrides */\n    div.sphinxsidebar {\n        position: relative !important;\n        float: none !important;\n        width: 230px !important;\n        max-width: 230px !important;\n        left: auto !important;\n        top: auto !important;\n        height: auto !important;\n        background-color: #300a24 !important;\n        box-shadow: none !important;\n        transition: none !important;\n        z-index: auto !important;\n        overflow-y: visible !important;\n        overflow-x: visible !important;\n        padding: 0 15px 15px 0 !important;\n        margin: 0 !important;\n        order: -1 !important;\n        flex-shrink: 0 !important;\n    }\n\n    /* Light text on dark background for desktop sidebar too */\n    div.sphinxsidebar h3 {\n        color: #e8ffca !important;\n    }\n\n    div.sphinxsidebar a {\n        color: #e8ffca !important;\n    }\n\n    div.sphinxsidebar a:hover {\n        color: #ffffff !important;\n    }\n\n    /* Ensure content wrapper uses flex layout - sidebar on LEFT */\n    div.documentwrapper {\n        float: left !important;\n        width: 100% !important;\n        margin-left: 0 !important;\n        flex-grow: 1 !important;\n    }\n\n    div.bodywrapper {\n        margin: 0 0 0 0px !important;\n        padding: 0 !important;\n    }\n\n    /* Clean margins */\n    div.document {\n        margin: 0 !important;\n        max-width: none !important;\n    }\n\n    div.body {\n        margin: 0 !important;\n    }\n}\n\n/* Large Screen Optimizations - Screens larger than 1024px */\n@media screen and (min-width: 1024px) {\n\n    /* Wider sidebar for large screens */\n    div.sphinxsidebar {\n        width: 300px !important;\n        max-width: 300px !important;\n    }\n\n    div.bodywrapper {\n        margin: 0 0 0 0px !important;\n    }\n\n    /* Just ensure clean margins, don't override layout */\n    div.document {\n        margin: 0 !important;\n        max-width: none !important;\n        display: flex !important;\n    }\n\n    div.documentwrapper {\n        margin: 0 !important;\n    }\n\n    div.body {\n        margin: 0 !important;\n    }\n\n    /* Images */\n    img {\n        max-width: 100%;\n        height: auto;\n    }\n\n    /* Code blocks with better readability */\n    div.highlight,\n    pre {\n        max-width: 100%;\n    }\n\n    /* Better table spacing */\n    table.docutils td,\n    table.docutils th {\n        padding: 10px !important;\n    }\n}\n\n/* Extra large screens - same as desktop */\n@media screen and (min-width: 1600px) {\n    div.document {\n        margin: 0 !important;\n        max-width: none !important;\n    }\n\n    div.body {\n        font-size: 16px;\n        line-height: 1.7;\n    }\n}\n\n/* Touch-friendly improvements for all mobile devices */\n@media (hover: none) and (pointer: coarse) {\n\n    /* Increase tap targets */\n    a,\n    button,\n    input[type=\"submit\"],\n    input[type=\"button\"] {\n        min-height: 44px;\n        min-width: 44px;\n        display: inline-flex;\n        align-items: center;\n        justify-content: center;\n        padding: 10px 15px;\n    }\n\n    /* Better spacing for touch */\n    div.sphinxsidebar a {\n        padding: 12px 5px;\n    }\n\n    /* Improve touch scrolling */\n    div.sphinxsidebar,\n    div.body,\n    div.highlight,\n    table,\n    .table-wrapper {\n        -webkit-overflow-scrolling: touch;\n    }\n\n    /* Remove hover states that don't work on touch */\n    *:hover {\n        -webkit-tap-highlight-color: rgba(0, 0, 0, 0.1);\n    }\n\n    /* Better focus states for accessibility */\n    a:focus,\n    button:focus,\n    input:focus {\n        outline: 3px solid #2980B9;\n        outline-offset: 2px;\n    }\n}\n\n/* Landscape orientation on mobile */\n@media screen and (max-width: 480px) and (orientation: landscape) {\n    div.sphinxsidebar {\n        width: 60% !important;\n        max-width: 400px !important;\n    }\n\n    div.body {\n        padding: 10px 15px !important;\n    }\n}\n\n/* High DPI / Retina displays */\n@media (-webkit-min-device-pixel-ratio: 2), (min-resolution: 192dpi) {\n    /* Sharper borders and shadows */\n    .mobile-menu-toggle {\n        border-width: 1px;\n    }\n\n    div.sphinxsidebar {\n        box-shadow: 2px 0 8px rgba(0, 0, 0, 0.25);\n    }\n}\n\n/* Reduce motion for users who prefer it */\n@media (prefers-reduced-motion: reduce) {\n    *,\n    *::before,\n    *::after {\n        animation-duration: 0.01ms !important;\n        animation-iteration-count: 1 !important;\n        transition-duration: 0.01ms !important;\n    }\n\n    div.sphinxsidebar {\n        transition: none !important;\n    }\n\n    .mobile-sidebar-overlay {\n        transition: none !important;\n    }\n}\n\n/* Print styles */\n@media print {\n\n    div.sphinxsidebar,\n    div.related,\n    .mobile-menu-toggle {\n        display: none !important;\n    }\n\n    div.documentwrapper {\n        width: 100% !important;\n        margin: 0 !important;\n    }\n\n    div.body {\n        padding: 0 !important;\n    }\n}\n"
  },
  {
    "path": "docs/_templates/footer.html",
    "content": "<div class=\"help-footer\">\n    <table style=\"margin: 0px auto;\">\n        <tbody>\n            <tr>\n                <td style=\"text-align: center; padding: 30px;\">\n                    <a href=\"http://stackoverflow.com/questions/tagged/chatterbot\">\n                        <div>\n                            <img src=\"{{ pathto('_static/so-icon.png', True) }}\" alt=\"Stack Overflow\">\n                        </div>\n                        <div>\n                            Ask a question under the<br/>\n                            chatterbot tag\n                        </div>\n                    </a>\n                </td>\n                <td style=\"text-align: center; padding: 30px;\">\n                    <a href=\"https://github.com/gunthercox/ChatterBot/issues/new\">\n                        <div>\n                            <img src=\"{{ pathto('_static/github-mark.png', True) }}\" alt=\"GitHub\">\n                        </div>\n                        <div>\n                            Report an issue on<br/>\n                            GitHub\n                        </div>\n                    </a>\n                </td>\n            </tr>\n            <tr>\n                <td colspan=\"2\">\n                    <a href=\"https://bsky.app/profile/chatterbot.us\">\n                        <div class=\"inline-block\">\n                            <svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 512 512\" class=\"bluesky-social-icon\"><!--!Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2025 Fonticons, Inc.--><path d=\"M111.8 62.2C170.2 105.9 233 194.7 256 242.4c23-47.6 85.8-136.4 144.2-180.2c42.1-31.6 110.3-56 110.3 21.8c0 15.5-8.9 130.5-14.1 149.2C478.2 298 412 314.6 353.1 304.5c102.9 17.5 129.1 75.5 72.5 133.5c-107.4 110.2-154.3-27.6-166.3-62.9l0 0c-1.7-4.9-2.6-7.8-3.3-7.8s-1.6 3-3.3 7.8l0 0c-12 35.3-59 173.1-166.3 62.9c-56.5-58-30.4-116 72.5-133.5C100 314.6 33.8 298 15.7 233.1C10.4 214.4 1.5 99.4 1.5 83.9c0-77.8 68.2-53.4 110.3-21.8z\" fill=\"#1185fe\"/></svg>\n                        </div>\n                        <div class=\"inline-block\">\n                            Subscribe to updates via<br/>\n                            @chatterbot.us on Bluesky\n                        </div>\n                    </a>\n                </td>\n            </tr>\n        </tbody>\n    </table>\n</div>\n\n<script type=\"text/javascript\" src=\"//s3.amazonaws.com/downloads.mailchimp.com/js/mc-validate.js\"></script>\n<script type='text/javascript'>\n    (function($) {\n        window.fnames = new Array();\n        window.ftypes = new Array();\n        fnames[0]='EMAIL';\n        ftypes[0]='email';\n        fnames[1]='FNAME';\n        ftypes[1]='text';\n        fnames[2]='LNAME';\n        ftypes[2]='text';\n    }(jQuery));\n    var $mcj = jQuery.noConflict(true);\n</script>\n"
  },
  {
    "path": "docs/_templates/layout.html",
    "content": "{% extends '!layout.html' %}\n{# https://github.com/sphinx-doc/sphinx/blob/master/sphinx/themes/basic/layout.html #}\n\n{% set pageurl = canonical_url() %}\n\n{%- block htmltitle %}\n    <meta name=\"google-site-verification\" content=\"uM9aB6XUTqBdf9f_THPuD1p8GplQU7kYLPAsrl1cORs\" />\n\n    <link rel=\"stylesheet\" id=\"silktide-consent-manager-css\" href=\"{{ pathto('_static/silktide-consent-manager.css', 1) }}\">\n    <script src=\"{{ pathto('_static/silktide-consent-manager.js', 1) }}\"></script>\n    <script>\n    silktideCookieBannerManager.updateCookieBannerConfig({\n      background: {\n        showBackground: true\n      },\n      cookieIcon: {\n        position: \"bottomRight\"\n      },\n      cookieTypes: [\n        {\n          id: \"analytics\",\n          name: \"Analytics\",\n          description: \"<p>These cookies are necessary for the website to function properly and cannot be switched off. They help with things like logging in and setting your privacy preferences.</p>\",\n          defaultValue: true,\n          required: false,\n          onAccept: function() {\n            gtag('consent', 'update', {\n              analytics_storage: 'granted',\n            });\n            dataLayer.push({\n              'event': 'consent_accepted_analytics',\n            });\n          },\n          onReject: function() {\n            gtag('consent', 'update', {\n              analytics_storage: 'denied',\n            });\n          }\n        },\n        {\n          id: \"advertising\",\n          name: \"Advertising\",\n          description: \"<p>These cookies help us improve the site by tracking which pages are most popular and how visitors move around the site.</p>\",\n          defaultValue: true,\n          required: false,\n          onAccept: function() {\n            gtag('consent', 'update', {\n              ad_storage: 'granted',\n              ad_user_data: 'granted',\n              ad_personalization: 'granted',\n            });\n            dataLayer.push({\n              'event': 'consent_accepted_advertising',\n            });\n          },\n          onReject: function() {\n            gtag('consent', 'update', {\n              ad_storage: 'denied',\n              ad_user_data: 'denied',\n              ad_personalization: 'denied',\n            });\n          }\n        }\n      ],\n      text: {\n        banner: {\n          description: \"<p>We use cookies on our site to enhance your user experience, provide personalized content, and analyze our traffic. <a href=\\\"https://chatterbot.us/privacy/\\\" target=\\\"_blank\\\">Cookie Policy.</a></p>\",\n          acceptAllButtonText: \"Accept all\",\n          acceptAllButtonAccessibleLabel: \"Accept all cookies\",\n          rejectNonEssentialButtonText: \"Reject non-essential\",\n          rejectNonEssentialButtonAccessibleLabel: \"Reject non-essential\",\n          preferencesButtonText: \"Preferences\",\n          preferencesButtonAccessibleLabel: \"Toggle preferences\"\n        },\n        preferences: {\n          title: \"Customize your cookie preferences\",\n          description: \"<p>We respect your right to privacy. You can choose not to allow some types of cookies. Your cookie preferences will apply across our website.</p>\",\n          creditLinkText: \"Get this banner for free\",\n          creditLinkAccessibleLabel: \"Get this banner for free\"\n        }\n      }\n    });\n    </script>\n\n    <meta name=\"google-adsense-account\" content=\"ca-pub-3019790405701706\">\n    <script async src=\"https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-3019790405701706\"\n    crossorigin=\"anonymous\"></script>\n\n{{ super() }}\n{%- endblock %}\n\n{%- block relbaritems %}\n    <li class=\"right\"><a href=\"{{github_page_link()}}\">Edit on GitHub</a> |</li>\n{% endblock %}\n\n{% block menu %}\n    {{ super() }}\n{% endblock %}\n\n{%- block footer %}\n    {{ super() }}\n    <!-- Google tag (gtag.js) -->\n    <script async src=\"https://www.googletagmanager.com/gtag/js?id=G-733BJS6NWQ\"></script>\n    <script>\n    window.dataLayer = window.dataLayer || [];\n    function gtag(){dataLayer.push(arguments);}\n\n    // Set consent defaults\n    // https://silktide.com/consent-manager/docs/google-consent-mode/\n    gtag('consent', 'default', {\n        analytics_storage: localStorage.getItem('silktideCookieChoice_analytics') === 'true' ? 'granted' : 'denied',\n        ad_storage: localStorage.getItem('silktideCookieChoice_advertising') === 'true' ? 'granted' : 'denied',\n        ad_user_data: localStorage.getItem('silktideCookieChoice_advertising') === 'true' ? 'granted' : 'denied',\n        ad_personalization: localStorage.getItem('silktideCookieChoice_advertising') === 'true' ? 'granted' : 'denied',\n    });\n\n    gtag('js', new Date());\n    gtag('config', 'G-733BJS6NWQ');\n    </script>\n{% endblock %}"
  },
  {
    "path": "docs/_templates/page.html",
    "content": "{# Template for simple pages #}\n{%- extends \"layout.html\" %}\n\n{% block body %}\n  {{ body }}\n  {%- include \"footer.html\" %}\n{% endblock %}\n"
  },
  {
    "path": "docs/_templates/sidebar_ad.html",
    "content": "<!-- ChatterBot Docs Ad -->\n<ins class=\"adsbygoogle\"\n     style=\"display:block\"\n     data-ad-client=\"ca-pub-3019790405701706\"\n     data-ad-slot=\"4239961764\"\n     data-ad-format=\"auto\"\n     data-full-width-responsive=\"true\"></ins>\n<script>\n     (adsbygoogle = window.adsbygoogle || []).push({});\n</script>\n"
  },
  {
    "path": "docs/chatterbot.rst",
    "content": "==========\nChatterBot\n==========\n\nThe main class ``ChatBot`` is a connecting point between each of\nChatterBot's :term:`adapters`. In this class, an input statement is\nprocessed and stored by the :term:`logic adapter` and :term:`storage adapter`.\nA response to the input is then generated and returned.\n\n.. autoclass:: chatterbot.ChatBot\n   :members:\n\nExample chat bot parameters\n===========================\n\n.. code-block:: python\n\n   ChatBot(\n       'Northumberland',\n       storage_adapter='my.storage.AdapterClass',\n       logic_adapters=[\n           'my.logic.AdapterClass1',\n           'my.logic.AdapterClass2'\n       ],\n       logger=custom_logger\n   )\n\n\nExample expanded chat bot parameters\n====================================\n\nIt is also possible to pass parameters directly to individual adapters.\nTo do this, you must use a dictionary that contains a key called ``import_path``\nwhich specifies the import path to the adapter class.\n\n.. code-block:: python\n\n   ChatBot(\n       'Leander Jenkins',\n       storage_adapter={\n           'import_path': 'my.storage.AdapterClass',\n           'database_uri': 'protocol://my-database'\n       },\n       logic_adapters=[\n           {\n               'import_path': 'my.logic.AdapterClass1',\n               'statement_comparison_function': chatterbot.comparisons.LevenshteinDistance\n               'response_selection_method': chatterbot.response_selection.get_first_response\n           },\n           {\n               'import_path': 'my.logic.AdapterClass2',\n               'statement_comparison_function': my_custom_comparison_function\n               'response_selection_method': my_custom_selection_method\n           }\n       ]\n   )\n\n\nEnable logging\n==============\n\nChatterBot has built in logging. You can enable ChatterBot's\nlogging by setting the logging level in your code.\n\n.. code-block:: python\n\n   import logging\n\n   logging.basicConfig(level=logging.INFO)\n\n   ChatBot(\n       # ...\n   )\n\nThe logging levels available are\n``CRITICAL``, ``ERROR``, ``WARNING``, ``INFO``, ``DEBUG``, and ``NOTSET``.\nSee the `Python logging documentation`_ for more information.\n\nUsing a custom logger\n=====================\n\nYou can choose to use your own custom logging class with your chat bot.\nThis can be useful when testing and debugging your code.\n\n.. code-block:: python\n\n   import logging\n\n   custom_logger = logging.getLogger(__name__)\n\n   ChatBot(\n       # ...\n       logger=custom_logger\n   )\n\nAdapters\n========\n\nChatterBot uses adapter modules to control the behavior of specific types of tasks.\nThere are four distinct types of adapters that ChatterBot uses,\nthese are storage adapters and logic adapters.\n\nAdapters types\n--------------\n\n1. Storage adapters - Provide an interface for ChatterBot to connect to various storage systems such as `MongoDB`_ or local file storage.\n2. Logic adapters - Define the logic that ChatterBot uses to respond to input it receives.\n\nAccessing the ChatBot instance\n-------------------------------\n\nWhen ChatterBot initializes each adapter, it sets an attribute named ``chatbot``.\nThe chatbot variable makes it possible for each adapter to have access to all of the other adapters being used.\nSuppose logic adapters need to share some information or perhaps you want to give your logic adapter\ndirect access to the storage adapter. These are just a few cases where this functionality is useful.\n\nEach adapter can be accessed on the chatbot object from within an adapter by referencing `self.chatbot`.\nThen, ``self.chatbot.storage`` refers to the storage adapter, and ``self.chatbot.logic`` refers to the logic adapters.\n\n.. _MongoDB: https://docs.mongodb.com/\n.. _`Python logging documentation`: https://docs.python.org/3/library/logging.html#logging-levels\n"
  },
  {
    "path": "docs/commands.rst",
    "content": "==================\nCommand line tools\n==================\n\nChatterBot comes with a few command line tools that can help\nwith general packaging-related tasks.\n\nGet the installed ChatterBot version\n====================================\n\nIf have ChatterBot installed and you want to check what version\nyou have then you can run the following command.\n\n.. code-block:: bash\n\n   python -m chatterbot --version\n\n\nList available commands\n=======================\n\nTo see a list of all available commands you can run the following:\n\n.. code-block:: bash\n\n   python -m chatterbot --help\n"
  },
  {
    "path": "docs/comparisons.rst",
    "content": "===========\nComparisons\n===========\n\n.. _statement-comparison:\n\nStatement comparison\n====================\n\nChatterBot uses ``Statement`` objects to hold information\nabout things that can be said. An important part of how a chat bot\nselects a response is based on its ability to compare two statements\nto each other. There are a number of ways to do this, and ChatterBot\ncomes with a handful of methods built in for you to use.\n\n.. automodule:: chatterbot.comparisons\n   :members:\n\nUse your own comparison function\n++++++++++++++++++++++++++++++++\n\nYou can create your own comparison function and use it as long as the function takes two statements\nas parameters and returns a numeric value between 0 and 1. A 0 should represent the lowest possible\nsimilarity and a 1 should represent the highest possible similarity.\n\n.. code-block:: python\n\n   def comparison_function(statement, other_statement):\n\n       # Your comparison logic\n\n       # Return your calculated value here\n       return 0.0\n\nSetting the comparison method\n-----------------------------\n\nTo set the statement comparison method for your chat bot, you\nwill need to pass the ``statement_comparison_function`` parameter\nto your chat bot when you initialize it. An example of this\nis shown below.\n\n.. code-block:: python\n\n   from chatterbot import ChatBot\n   from chatterbot.comparisons import LevenshteinDistance\n\n   chatbot = ChatBot(\n       # ...\n       statement_comparison_function=LevenshteinDistance\n   )\n\n\nTaggers\n=======\n\nChatterBot supports a number of different taggers that can be used to\nprocess the input text. The taggers are used to identify the parts of speech\nin the input text and can be used to improve the accuracy of the response selection.\n\n.. automodule:: chatterbot.tagging\n   :members:\n   :undoc-members:\n\nLanguages\n=========\n\nChatterBot's ``languages`` module contains helper classes for working with\nlanguage codes and names.\n\n.. autoclass:: chatterbot.languages.ENG\n\n.. autoclass:: chatterbot.languages.FRE\n\n.. autoclass:: chatterbot.languages.GER\n\n.. autoclass:: chatterbot.languages.ITA\n\n.. autoclass:: chatterbot.languages.JPN\n\n.. autoclass:: chatterbot.languages.KOR\n\n.. autoclass:: chatterbot.languages.POR\n\n.. autoclass:: chatterbot.languages.RUS\n\n.. autoclass:: chatterbot.languages.SPA\n\n.. autoclass:: chatterbot.languages.SWE\n\n.. autoclass:: chatterbot.languages.TUR\n\n.. autoclass:: chatterbot.languages.ZHT\n\nSee ``chatterbot.languages`` for the full list of languages.\n"
  },
  {
    "path": "docs/conf.py",
    "content": "import os\nimport sys\nfrom pathlib import Path\nfrom datetime import datetime\n\n\ncurrent_directory = os.path.dirname(os.path.abspath(__file__))\nparent_directory = os.path.abspath(os.path.join(current_directory, os.pardir))\n\n# Insert the project root dir as the first element in the PYTHONPATH.\n# This lets us ensure that the source package is imported, and used to generate the documentation.\nsys.path.insert(0, parent_directory)\n\nsys.path.append(str(Path('_ext').resolve()))\n\nfrom chatterbot import __version__ as chatterbot_version\n\n# Sphinx extension modules\nextensions = [\n    'sphinx.ext.autodoc',\n    'sphinx.ext.autosectionlabel',\n    'sphinx.ext.coverage',\n    'sphinx.ext.doctest',\n    'sphinx.ext.intersphinx',\n    'sphinx.ext.mathjax',\n    'sphinx.ext.todo',\n    'sphinx.ext.viewcode',\n    'github',\n    'canonical',\n    'sphinx_sitemap',\n]\n\n# Sphinx Sitemap Plugin Configuration\n\nsitemap_url_scheme = '{link}'\n\nsitemap_prettify = True\n\n# Add any paths that contain templates here, relative to this directory.\ntemplates_path = ['_templates']\n\n# The suffix(es) of source filenames.\n# You can specify multiple suffix as a list of string:\nsource_suffix = ['.rst', '.md']\n\n# The encoding of source files\n# source_encoding = 'utf-8-sig'\n\n# The master toctree document\nmaster_doc = 'index'\n\n# General information about the project\nproject = 'ChatterBot'\nauthor = 'Gunther Cox'\ncopyright = '{}, {}'.format(\n    datetime.now().year,\n    author\n)\n\n# The full version, including alpha/beta/rc tags\nrelease = chatterbot_version\n\n# The short X.Y version\nversion = chatterbot_version.rsplit('.', 1)[0]\n\nlanguage = 'en'\n\n# List of patterns, relative to source directory, that match files and\n# directories to ignore when looking for source files.\n# This patterns also effect to html_static_path and html_extra_path\nexclude_patterns = []\n\n# If true, '()' will be appended to :func: etc. cross-reference text\n# add_function_parentheses = True\n\n# If true, the current module name will be prepended to all description\n# unit titles (such as .. function::)\n# add_module_names = True\n\n# If true, sectionauthor and moduleauthor directives will be shown in the\n# output. They are ignored by default.\n# show_authors = False\n\n# The name of the Pygments (syntax highlighting) style to use\npygments_style = 'sphinx'\n\n# -- Options for HTML output ----------------------------------------------\n\nhtml_theme = 'classic'\n\n# Theme options are theme-specific and customize the look and feel of a theme\n# further. For a list of options available for each theme, see the\n# documentation:\n# https://www.sphinx-doc.org/en/master/usage/theming.html\nhtml_theme_options = {\n    'externalrefs': True,\n    'sidebarbgcolor': '#300a24',\n    'relbarbgcolor': '#26001b',\n    'footerbgcolor': '#13000d',\n    'headbgcolor': '#503949',\n    'headtextcolor': '#e8ffca',\n    'headlinkcolor': '#e8ffca',\n    'sidebarwidth': '300px',\n    # 'collapsiblesidebar': True,\n}\n\nroot_doc = 'index'\n\nhtml_show_sourcelink = True\n\nhtml_baseurl = 'https://docs.chatterbot.us/'\n\n# A shorter title for the navigation bar. Default is the same as html_title.\n# html_short_title = None\n\n# The name of an image file (relative to this directory) to place at the top\n# of the sidebar.\nhtml_logo = '../graphics/banner.png'\n\n# The name of an image file (relative to this directory) to use as a favicon of\n# the docs.  This file should be a Windows icon file (.ico) being 16x16 or 32x32\n# pixels large.\nhtml_favicon = '_static/favicon.ico'\n\n# Add any paths that contain custom static files (such as style sheets) here,\n# relative to this directory. They are copied after the builtin static files,\n# so a file named \"default.css\" will overwrite the builtin \"default.css\".\nhtml_static_path = ['_static']\n\n# Add any extra paths that contain custom files (such as robots.txt or\n# .htaccess) here, relative to this directory. These files are copied\n# directly to the root of the documentation.\nhtml_extra_path = [\n    'robots.txt',\n]\n\n# If not None, a 'Last updated on:' timestamp is inserted at every page\n# bottom, using the given strftime format.\n# The empty string is equivalent to '%b %d, %Y'.\nhtml_last_updated_fmt = None\n\n# Custom sidebar templates, maps document names to template names.\nhtml_sidebars = {\n    '**': ['searchbox.html', 'globaltoc.html', 'sidebar_ad.html']\n}\n\nhtml_css_files = [\n    'style.css',\n    'silktide-consent-manager.css'\n]\n\nhtml_js_files = [\n    'mobile.js',\n    'silktide-consent-manager.js'\n]\n\n# Additional templates that should be rendered to pages, maps page names to\n# template names.\n# html_additional_pages = {}\n\n# If false, no module index is generated.\nhtml_domain_indices = True\n\n# If false, no index is generated.\nhtml_use_index = True\n\n# Split the index into individual pages for each letter.\nhtml_split_index = True\n\n# If true, \"Created using Sphinx\" is shown in the HTML footer. Default is True.\nhtml_show_sphinx = False\n\n# Language to be used for generating the HTML full-text search index.\n# Sphinx supports the following languages:\n#   'da', 'de', 'en', 'es', 'fi', 'fr', 'hu', 'it', 'ja'\n#   'nl', 'no', 'pt', 'ro', 'ru', 'sv', 'tr', 'zh'\nhtml_search_language = 'en'\n\n# Output file base name for HTML help builder\nhtmlhelp_basename = 'ChatterBotdoc'\n\n# Theme modifications\n\nhtml_context = {\n    'extra_css_files': [\n        '_static/style.css'\n    ]\n}\n\n# Grouping the document tree into LaTeX files. List of tuples\n# (source start file, target name, title,\n#  author, documentclass [howto, manual, or own class])\nlatex_documents = [\n    (master_doc, 'ChatterBot.tex', u'ChatterBot Documentation',\n     u'Gunther Cox', 'manual'),\n]\n\n# -- Options for manual page output ---------------------------------------\n\n# One entry per manual page. List of tuples\n# (source start file, name, description, authors, manual section)\nman_pages = [\n    (master_doc, 'chatterbot', u'ChatterBot Documentation',\n     [author], 1)\n]\n\n# -- Options for Texinfo output -------------------------------------------\n\n# Grouping the document tree into Texinfo files. List of tuples\n# (source start file, target name, title, author,\n#  dir menu entry, description, category)\ntexinfo_documents = [\n    (master_doc, 'ChatterBot', u'ChatterBot Documentation',\n     author, 'ChatterBot', 'One line description of project.',\n     'Miscellaneous'),\n]\n\n# -- Options for Epub output ----------------------------------------------\n\n# Bibliographic Dublin Core info\nepub_title = project\nepub_author = author\nepub_publisher = author\nepub_copyright = copyright\n\n# A list of files that should not be packed into the epub file\nepub_exclude_files = ['search.html']\n\n# Configuration for intersphinx\nintersphinx_mapping = {\n    'python': ('https://docs.python.org/3', None),\n    'mathparse': ('https://mathparse.chatterbot.us/', None),\n}\n"
  },
  {
    "path": "docs/contributing.rst",
    "content": "==========================\nContributing to ChatterBot\n==========================\n\nThere are numerous ways to contribute to ChatterBot. All of which are highly encouraged.\n\n- Contributing bug reports and feature requests\n\n- Contributing documentation\n\n- Contributing code for new features\n\n- Contributing fixes for bugs\n\nEvery bit of help received on this project is highly appreciated.\n\n\nSetting Up a Development Environment\n====================================\n\nTo contribute to ChatterBot's development, you simply need:\n\n- Python\n\n- pip\n\n- A few python packages. You can install them from this projects ``pyproject.yml`` file by running:\n\n.. code-block:: bash\n\n   pip .[dev,test]\n\n- A text editor\n\n\nReporting a Bug\n===============\n\nIf you discover a bug in ChatterBot and wish to report it, please be\nsure that you adhere to the following when you report it on GitHub.\n\n1. Before creating a new bug report, please search to see if an open or closed report matching yours already exists.\n2. Please include a description that will allow others to recreate the problem you encountered.\n\n\nRequesting New Features\n=======================\n\nWhen requesting a new feature in ChatterBot, please make sure to include\nthe following details in your request.\n\n1. Your use case. Describe what you are doing that requires this new functionality.\n\n\nContributing Documentation\n==========================\n\nChatterBot's documentation is written in reStructuredText and is\ncompiled by Sphinx. The reStructuredText source of the documentation\nis located in the ``docs/`` directory.\n\nTo build the documentation yourself, run:\n\n.. code-block:: bash\n\n    sphinx-build -nW -b dirhtml docs/ html/\n\nA useful way to view the documentation is to use the Python built-in HTTP server. You can do this by running:\n\n.. code-block:: bash\n\n    python -m http.server\n\nThen navigate to ``http://localhost:8000/`` in your web browser.\n\nContributing Code\n=================\n\nThe development of ChatterBot happens on GitHub. Code contributions should be\nsubmitted there in the form of pull requests.\n\nPull requests should meet the following criteria.\n\n1. Fix one issue and fix it well.\n2. Do not include extraneous changes that do not relate to the issue being fixed.\n3. Include a descriptive title and description for the pull request.\n4. Have descriptive commit messages.\n\nDevelopment pattern for contributors\n====================================\n\n1. [Create a fork](https://help.github.com/articles/fork-a-repo/) of\n   the [main ChatterBot repository](https://github.com/gunthercox/ChatterBot) on GitHub.\n2. Make your changes in a branch named something different from `master`, e.g. create\n   a new branch `my-pull-request`.\n3. [Create a pull request](https://help.github.com/articles/creating-a-pull-request/).\n4. Please follow the [Python style guide for PEP-8](https://www.python.org/dev/peps/pep-0008/).\n5. Use the projects [built-in testing](https://docs.chatterbot.us/testing/).\n   to help make sure that your contribution is free from errors.\n"
  },
  {
    "path": "docs/conversations.rst",
    "content": "=============\nConversations\n=============\n\nChatterBot supports the ability to have multiple concurrent conversations.\nA conversations is where the :term:`chat bot` interacts with a person, and supporting\nmultiple concurrent conversations means that the chat bot can have multiple\ndifferent conversations with different people at the same time.\n\nConversation scope\n------------------\n\nIf two ``ChatBot`` instances are created, each will have conversations separate from each other.\n\nAn adapter can access any conversation as long as the unique identifier for the conversation is provided.\n\nConversation example\n--------------------\n\nThe following example is taken from the Django ``ChatterBotApiView`` built into ChatterBot.\nIn this method, the unique identifiers for each chat session are being stored in Django's\nsession objects. This allows different users who interact with the bot through different\nweb browsers to have separate conversations with the chat bot.\n\n.. literalinclude:: ../examples/django_example/django_example/views.py\n   :caption: examples/django_example/django_example/views.py\n   :language: python\n   :pyobject: ChatterBotApiView.post\n   :dedent: 4\n\n\n..  _conversation_statements:\n\nStatements\n==========\n\nChatterBot's statement objects represent either an input statement that the\nchat bot has received from a user, or an output statement that the chat bot\nhas returned based on some input.\n\n.. autoclass:: chatterbot.conversation.Statement\n   :members:\n\n   .. autoattribute:: chatterbot.conversation.Statement.confidence\n\n      ChatterBot's logic adapters assign a confidence score to the statement\n      before it is returned. The confidence score indicates the degree of\n      certainty with which the chat bot believes this is the correct response\n      to the given input.\n\n   .. autoattribute:: chatterbot.conversation.Statement.in_response_to\n\n      The response attribute represents the relationship between two statements.\n      This value of this field indicates that one statement was issued in response\n      to another statement.\n\n\nStatement-response relationship\n===============================\n\nChatterBot stores knowledge of conversations as statements. Each statement can have any\nnumber of possible responses.\n\n.. image:: _static/statement-response-relationship.svg\n   :alt: ChatterBot statement-response relationship\n\nEach ``Statement`` object has an ``in_response_to`` reference which links the\nstatement to a number of other statements that it has been learned to be in response to.\nThe ``in_response_to`` attribute is essentially a reference to all parent statements\nof the current statement.\n\n.. image:: _static/statement-relationship.svg\n   :alt: ChatterBot statement relationship\n\nThe count of recorded statements with matching, or similar text indicates the number of\ntimes that the statement has been given as a response. This makes it possible for the\nchat bot to determine if a particular response is more commonly used than another.\n"
  },
  {
    "path": "docs/corpus.rst",
    "content": "ChatterBot Corpus\n=================\n\nThis is a :term:`corpus` of dialog data that is included in the chatterbot module.\n\nAdditional information about the ``chatterbot-corpus`` module can be found\nin the `ChatterBot Corpus Documentation`_.\n\nCorpus language availability\n----------------------------\n\nCorpus data is user contributed, but it is also not difficult to create one if you are familiar with the language.\nThis is because each corpus is just a sample of various input statements and their responses for the bot to train itself with.\n\nTo explore what languages and collections of corpora are available,\ncheck out the `chatterbot_corpus/data`_ directory in the separate chatterbot-corpus repository.\n\n.. note::\n   If you are interested in contributing content to the corpus, please feel free to\n   submit a pull request on ChatterBot's corpus GitHub page. Contributions are welcomed!\n\n   https://github.com/gunthercox/chatterbot-corpus\n\n   The ``chatterbot-corpus`` is distributed in its own Python package so that it can\n   be released and upgraded independently from the ``chatterbot`` package.\n\n\nExporting your chat bot's database as a training corpus\n-------------------------------------------------------\n\nNow that you have created your chat bot and sent it out into the world, perhaps\nyou are looking for a way to share what it has learned with other chat bots?\nChatterBot's training module provides methods that allow you to export the\ncontent of your chat bot's database as a training corpus that can be used to\ntrain other chat bots.\n\nHere is an example:\n\n.. literalinclude:: ../examples/export_example.py\n   :caption: /examples/export_example.py\n   :language: python\n\n.. _chatterbot_corpus/data: https://github.com/gunthercox/chatterbot-corpus/tree/master/chatterbot_corpus/data\n.. _ChatterBot Corpus Documentation: https://corpus.chatterbot.us/\n"
  },
  {
    "path": "docs/development.rst",
    "content": "===========\nDevelopment\n===========\n\nAs the code for ChatterBot is written, the developers attempt to describe\nthe logic and reasoning for the various decisions that go into creating the\ninternal structure of the software. This internal documentation is intended\nfor future developers and maintainers of the project. A majority of this\ninformation is unnecessary for the typical developer using ChatterBot.\n\nIt is not always possible for every idea to be documented. As a result, the\nneed may arise to question the developers and maintainers of this project\nin order to pull concepts from their minds and place them in these documents.\nPlease pull gently.\n\n.. toctree::\n    :maxdepth: 2\n    :caption: Contents:\n\n    contributing\n    releases\n    Release Notes <https://github.com/gunthercox/ChatterBot/releases>\n    testing\n    packaging\n\nSuggested Development Tools\n===========================\n\nTo help developers work with ChatterBot and projects built using it, the\nfollowing tools are suggested. Keep in mind that none of these are required,\nbut this list has been assembled because it is often useful to have a\ntool or technology recommended by people who have experience using it. \n\nText Editors\n------------\n\nVisual Studio Code\n++++++++++++++++++\n\nWebsite: https://code.visualstudio.com/\n\n| I find Visual Studio Code to be an optimally light-weight\n| and versatile editor.\n|\n| ~ Gunther Cox\n\nDatabase Clients\n----------------\n\nSQLite Viewer\n+++++++++++++\n\n| This is a very simple SQLite viewer extension for VS Code that allows you to\n| sqlite databases.\n\nWebsite: https://marketplace.visualstudio.com/items?itemName=qwtel.sqlite-viewer\n\n\npgAdmin\n+++++++\n\n| pgAdmin a pretty good database client when working with PostgreSQL. \n\nWebsite: https://www.pgadmin.org/"
  },
  {
    "path": "docs/django/custom-models.rst",
    "content": "=======================\nCustom Statement Models\n=======================\n\nChatterBot allows you to replace the default Statement and Tag models with custom versions,\nusing a pattern similar to Django's ``AUTH_USER_MODEL`` setting. This enables you to add\ncustom fields, business logic, and integrations specific to your application.\n\nWhy Use Custom Models?\n========================\n\nCustom models allow you to:\n\n- **Add custom fields** - User ratings, sentiment scores, metadata, timestamps\n- **Implement business logic** - Custom validation, automated tagging, calculations\n- **Integrate with existing schemas** - Foreign keys to User models, related data\n- **Optimize performance** - Custom indexes, database-specific features\n- **Track analytics** - Views, feedback, response times\n\nCreating Custom Models\n========================\n\nStep 1: Inherit from Abstract Base Classes\n------------------------------------------\n\nChatterBot provides abstract base classes that define the required fields and methods.\nInherit from these to create your custom models:\n\n.. code-block:: python\n   :caption: myapp/models.py\n\n   from chatterbot.ext.django_chatterbot.abstract_models import (\n       AbstractBaseStatement,\n       AbstractBaseTag\n   )\n   from django.db import models\n   from django.contrib.auth import get_user_model\n\n   User = get_user_model()\n\n\n   class CustomStatement(AbstractBaseStatement):\n       \"\"\"\n       Custom Statement model with user feedback tracking.\n       \"\"\"\n\n       # Add your custom fields\n       upvotes = models.IntegerField(default=0)\n       downvotes = models.IntegerField(default=0)\n       sentiment_score = models.FloatField(null=True, blank=True)\n       metadata = models.JSONField(default=dict, blank=True)\n\n       # Track which users interacted\n       upvoted_by = models.ManyToManyField(\n           User,\n           related_name='upvoted_statements',\n           blank=True\n       )\n\n       # Required: ManyToMany relationship to a Tag model\n       # (Can be a custom Tag model or the default)\n       tags = models.ManyToManyField(\n           'CustomTag',\n           related_name='statements',\n           blank=True\n       )\n\n       class Meta:\n           swappable = 'CHATTERBOT_STATEMENT_MODEL'\n           indexes = [\n               models.Index(fields=['upvotes']),\n               models.Index(fields=['sentiment_score']),\n           ]\n\n       # Custom properties and methods\n       @property\n       def score(self):\n           \"\"\"\n           Calculate net score.\n           \"\"\"\n           return self.upvotes - self.downvotes\n\n       def upvote(self, user):\n           \"\"\"\n           Record an upvote from a user.\n           \"\"\"\n           if not self.upvoted_by.filter(pk=user.pk).exists():\n               self.upvoted_by.add(user)\n               self.upvotes += 1\n               self.save()\n\n\n   class CustomTag(AbstractBaseTag):\n       \"\"\"\n       Custom Tag model with categorization.\n       \"\"\"\n\n       # Add your custom fields\n       category = models.CharField(max_length=64, blank=True)\n       is_active = models.BooleanField(default=True)\n       priority = models.IntegerField(default=0)\n\n       class Meta:\n           swappable = 'CHATTERBOT_TAG_MODEL'\n           ordering = ['-priority', 'name']\n\nStep 2: Configure Django Settings\n----------------------------------\n\nTell ChatterBot to use your custom models by adding these settings:\n\n.. code-block:: python\n   :caption: settings.py\n\n   # Swappable model configuration (like AUTH_USER_MODEL)\n   CHATTERBOT_STATEMENT_MODEL = 'myapp.CustomStatement'\n   CHATTERBOT_TAG_MODEL = 'myapp.CustomTag'\n\n   # Standard ChatterBot configuration\n   CHATTERBOT = {\n       'name': 'My Bot',\n       'storage_adapter': 'chatterbot.storage.DjangoStorageAdapter',\n       'logic_adapters': [\n           'chatterbot.logic.BestMatch',\n       ]\n   }\n\n   # Add your app to INSTALLED_APPS\n   INSTALLED_APPS = [\n       # ... other apps\n       'django_chatterbot',  # ChatterBot's Django app\n       'myapp',  # Your app with custom models\n   ]\n\nStep 3: Create and Run Migrations\n----------------------------------\n\nGenerate and apply migrations for your custom models:\n\n.. code-block:: bash\n\n   # Create migrations for your custom models\n   python manage.py makemigrations myapp\n\n   # Apply migrations\n   python manage.py migrate\n\nChatterBot will now use your custom models automatically.\n\nRequired Fields and Methods\n============================\n\nYour custom Statement model **must include**:\n\nRequired Fields (from AbstractBaseStatement)\n---------------------------------------------\n\n- ``text`` - The statement text (CharField, max_length from constants)\n- ``search_text`` - Indexed search text (CharField)\n- ``conversation`` - Conversation identifier (CharField)\n- ``created_at`` - Timestamp (DateTimeField)\n- ``in_response_to`` - Previous statement text (CharField, nullable)\n- ``search_in_response_to`` - Indexed previous text (CharField)\n- ``persona`` - Speaker identifier (CharField)\n- ``tags`` - ManyToManyField to your Tag model\n\nRequired Methods\n----------------\n\n- ``get_tags()`` - Returns list of tag name strings\n- ``add_tags(*tags)`` - Adds tags to the statement\n- ``__str__()`` - String representation\n\n.. note::\n   These methods are already implemented in ``AbstractBaseStatement`` and will work correctly \n   with your custom Tag model through automatic detection. You only need to override them if \n   you need custom behavior.\n\nYour custom Tag model **must include**:\n\nRequired Fields (from AbstractBaseTag)\n--------------------------------------\n\n- ``name`` - Unique tag name (SlugField, unique=True)\n\nAlternative: Per-Instance Configuration\n=======================================\n\nYou can also specify custom models per ChatBot instance without changing Django settings.\nThis is useful when running multiple bots with different schemas:\n\n.. code-block:: python\n   :caption: views.py\n\n   from chatterbot import ChatBot\n\n   # Bot with custom models\n   custom_bot = ChatBot(\n       'Custom Bot',\n       storage_adapter='chatterbot.storage.DjangoStorageAdapter',\n       statement_model='myapp.CustomStatement',\n       tag_model='myapp.CustomTag'\n   )\n\n   # Bot with default models\n   default_bot = ChatBot(\n       'Default Bot',\n       storage_adapter='chatterbot.storage.DjangoStorageAdapter'\n   )\n\nThe ``statement_model`` and ``tag_model`` kwargs take precedence over Django settings.\n\nTag Model Detection\n-------------------\n\nChatterBot automatically detects which Tag model to use through a smart fallback system:\n\n1. **Django Settings** - First checks ``CHATTERBOT_TAG_MODEL`` setting (project-wide config)\n2. **Field Introspection** - If no setting exists, introspects the Statement model's ``tags`` \n   field to determine which Tag model it references (handles per-instance kwargs)\n3. **Default Fallback** - Falls back to ``'django_chatterbot.Tag'`` if detection fails\n\nThis ensures that ``add_tags()`` always uses the correct Tag model, whether you configure\ncustom models via Django settings or storage adapter kwargs.\n\n.. code-block:: python\n   :caption: Example: Field introspection in action\n\n   # When you define your Statement model like this:\n   class CustomStatement(AbstractBaseStatement):\n       tags = models.ManyToManyField('CustomTag', related_name='statements')\n       # ...\n\n   # ChatterBot's get_tag_model() will introspect the tags field\n   # and automatically detect that it should use CustomTag,\n   # even if CHATTERBOT_TAG_MODEL is not set in settings.\n\nThis automatic detection is especially important when using the per-instance configuration\napproach, as it ensures consistent behavior without requiring Django settings changes.\n\nUsage Examples\n==============\n\nExample: User Feedback\n----------------------\n\nTrack user ratings on responses:\n\n.. code-block:: python\n   :caption: myapp/models.py\n\n   from chatterbot.ext.django_chatterbot.abstract_models import AbstractBaseStatement\n   from django.db import models\n   from django.contrib.auth import get_user_model\n\n   User = get_user_model()\n\n   class FeedbackStatement(AbstractBaseStatement):\n       \"\"\"\n       Statement with user feedback.\n       \"\"\"\n\n       helpful_count = models.IntegerField(default=0)\n       not_helpful_count = models.IntegerField(default=0)\n\n       rated_by = models.ManyToManyField(\n           User,\n           through='StatementRating',\n           related_name='rated_statements'\n       )\n\n       tags = models.ManyToManyField(\n           'Tag',\n           related_name='statements'\n       )\n\n       class Meta:\n           swappable = 'CHATTERBOT_STATEMENT_MODEL'\n\n   class StatementRating(models.Model):\n       \"\"\"\n       Track individual user ratings.\n       \"\"\"\n       user = models.ForeignKey(User, on_delete=models.CASCADE)\n       statement = models.ForeignKey(FeedbackStatement, on_delete=models.CASCADE)\n       is_helpful = models.BooleanField()\n       created_at = models.DateTimeField(auto_now_add=True)\n\n       class Meta:\n           unique_together = ('user', 'statement')\n\n.. code-block:: python\n   :caption: myapp/views.py\n\n   from django.http import JsonResponse\n   from django.views.decorators.http import require_POST\n   from django.contrib.auth.decorators import login_required\n   from myapp.models import FeedbackStatement, StatementRating\n\n   @login_required\n   @require_POST\n   def rate_statement(request, statement_id):\n       statement = FeedbackStatement.objects.get(pk=statement_id)\n       is_helpful = request.POST.get('helpful') == 'true'\n\n       rating, created = StatementRating.objects.update_or_create(\n           user=request.user,\n           statement=statement,\n           defaults={'is_helpful': is_helpful}\n       )\n\n       # Update counts\n       statement.helpful_count = statement.rated_by.filter(\n           statementrating__is_helpful=True\n       ).count()\n       statement.not_helpful_count = statement.rated_by.filter(\n           statementrating__is_helpful=False\n       ).count()\n       statement.save()\n\n       return JsonResponse({\n           'helpful': statement.helpful_count,\n           'not_helpful': statement.not_helpful_count\n       })\n\nMigrating from Default Models\n=============================\n\nIf you need to migrate an existing project from default to custom models:\n\n1. Create Custom Models\n-----------------------\n\nCreate your custom models inheriting from the abstract base classes.\n\n2. Update Settings\n------------------\n\nAdd ``CHATTERBOT_STATEMENT_MODEL`` and ``CHATTERBOT_TAG_MODEL`` to settings.\n\n3. Create Initial Migration\n---------------------------\n\n.. code-block:: bash\n\n   python manage.py makemigrations myapp\n\n4. Create Data Migration\n-------------------------\n\nWrite a data migration to copy existing data:\n\n.. code-block:: python\n   :caption: myapp/migrations/0002_copy_chatterbot_data.py\n\n   from django.db import migrations\n\n   def copy_statements(apps, schema_editor):\n       # Get old and new models\n       OldStatement = apps.get_model('django_chatterbot', 'Statement')\n       NewStatement = apps.get_model('myapp', 'CustomStatement')\n       OldTag = apps.get_model('django_chatterbot', 'Tag')\n\n       # Copy all statements\n       for old_stmt in OldStatement.objects.all():\n           new_stmt = NewStatement.objects.create(\n               text=old_stmt.text,\n               search_text=old_stmt.search_text,\n               conversation=old_stmt.conversation,\n               created_at=old_stmt.created_at,\n               in_response_to=old_stmt.in_response_to,\n               search_in_response_to=old_stmt.search_in_response_to,\n               persona=old_stmt.persona,\n           )\n\n           # Copy tags\n           new_stmt.tags.set(old_stmt.tags.all())\n\n   def reverse_copy(apps, schema_editor):\n       # Optionally implement reverse migration\n       pass\n\n   class Migration(migrations.Migration):\n       dependencies = [\n           ('myapp', '0001_initial'),\n           ('django_chatterbot', '__latest__'),\n       ]\n\n       operations = [\n           migrations.RunPython(copy_statements, reverse_copy),\n       ]\n\n5. Apply Migrations\n-------------------\n\n.. code-block:: bash\n\n   python manage.py migrate myapp\n\nBest Practices\n==============\n\n1. **Always inherit from abstract base classes** - This ensures compatibility with ChatterBot's storage adapter.\n\n2. **Set swappable in Meta** - Add ``swappable = 'CHATTERBOT_STATEMENT_MODEL'`` to enable model swapping.\n\n3. **Implement required methods** - ``get_tags()`` and ``add_tags()`` are essential for ChatterBot's operation.\n\n4. **Use indexes wisely** - Add database indexes to fields you'll frequently query or filter on.\n\n5. **Test thoroughly** - Test your custom models with ChatterBot's training and conversation features.\n\n6. **Document your schema** - Clearly document any custom fields and their purposes for future maintainers.\n\nTroubleshooting\n===============\n\nModel Not Found Error\n---------------------\n\nIf you see errors like ``LookupError: No installed app with label 'myapp'``:\n\n- Ensure your app is in ``INSTALLED_APPS``\n- Check that migrations have been run\n- Verify the model path in settings (format: ``'app_label.ModelName'``)\n\nTags Relationship Error\n-----------------------\n\nIf you get errors about the tags relationship:\n\n- Ensure your Statement model has a ``tags`` ManyToManyField\n- The field must reference your custom Tag model (e.g., ``'CustomTag'`` or ``'myapp.CustomTag'``)\n- Use ``related_name='statements'``\n\n.. note::\n   ChatterBot automatically detects which Tag model to use by introspecting your Statement \n   model's ``tags`` field. This means ``add_tags()`` will work correctly even when you \n   specify custom models via storage adapter kwargs instead of Django settings.\n\nMigration Conflicts\n-------------------\n\nIf migrations conflict:\n\n- Run ``python manage.py makemigrations --merge``\n- Check migration dependencies\n- Ensure ChatterBot is migrated before your custom models\n\nSee Also\n========\n\n- :doc:`settings` - Django settings reference\n- :doc:`index` - Django integration overview\n- :doc:`/training` - Training your chatbot\n"
  },
  {
    "path": "docs/django/index.rst",
    "content": "==================\nDjango Integration\n==================\n\nChatterBot has direct support for integration with Django's ORM.\nIt is relatively easy to use ChatterBot within your Django application\nto create conversational pages and endpoints.\n\nInstall packages\n================\n\nBegin by making sure that you have installed both ``django`` and ``chatterbot``.\n\n.. sourcecode:: sh\n\n   pip install django chatterbot\n\nFor more details on installing and using Django, see the `Django documentation`_.\n\nInstalled Apps\n--------------\n\nAdd ``chatterbot.ext.django_chatterbot`` to your ``INSTALLED_APPS`` in the\n``settings.py`` file of your Django project.\n\n.. code-block:: python\n\n   INSTALLED_APPS = (\n       # ...\n       'chatterbot.ext.django_chatterbot',\n   )\n\n\nMigrations\n----------\n\nYou can run the Django database migrations for your chat bot with the\nfollowing command.\n\n.. sourcecode:: sh\n\n   python manage.py migrate django_chatterbot\n\nMongoDB and Django\n------------------\n\nChatterBot has a storage adapter for MongoDB but it does not work with Django.\nIf you want to use MongoDB as your database for Django and your chat bot then\nyou will need to install a **Django storage backend** such as `Django MongoDB Engine`_.\n\nThe reason this is required is because Django's storage backends are different\nand completely separate from ChatterBot's storage adapters.\n\nDjango App Development\n======================\n\n.. toctree::\n   :maxdepth: 2\n\n   settings\n   custom-models\n   views\n   wsgi\n\nGeneral Django Tutorials\n========================\n\nThese are general tutorials related to Django and are not specific to ChatterBot. The goal of these tutorials is to help provide a foundation of knowledge around building Django applications and APIs.\n\n.. toctree::\n   :maxdepth: 4\n\n   tutorial/index\n\n.. _Django documentation: https://docs.djangoproject.com/en/dev/intro/install/\n.. _Django MongoDB Engine: https://django-mongodb-engine.readthedocs.io/\n"
  },
  {
    "path": "docs/django/settings.rst",
    "content": "==========================\nChatterBot Django Settings\n==========================\n\nYou can edit the ChatterBot configuration through your Django settings.py file.\n\n.. code-block:: python\n   :caption: settings.py\n\n   CHATTERBOT = {\n       'name': 'Tech Support Bot',\n       'logic_adapters': [\n           'chatterbot.logic.MathematicalEvaluation',\n           'chatterbot.logic.TimeLogicAdapter',\n           'chatterbot.logic.BestMatch'\n       ]\n   }\n\nAny setting that gets set in the CHATTERBOT dictionary will be passed to the chat bot that powers your django app.\n\nAdditional Django settings\n==========================\n\n- ``django_app_name`` [default: 'django_chatterbot'] The Django app name to look up the models from.\n- ``database`` [default: 'default'] The Django database alias to use for ChatterBot data storage.\n\nSwappable Model Settings\n=========================\n\nChatterBot supports custom Statement and Tag models similar to Django's ``AUTH_USER_MODEL`` pattern:\n\n- ``CHATTERBOT_STATEMENT_MODEL`` [default: 'django_chatterbot.Statement'] The Statement model to use.\n- ``CHATTERBOT_TAG_MODEL`` [default: 'django_chatterbot.Tag'] The Tag model to use.\n\n.. code-block:: python\n   :caption: settings.py\n\n   # Use custom models\n   CHATTERBOT_STATEMENT_MODEL = 'myapp.CustomStatement'\n   CHATTERBOT_TAG_MODEL = 'myapp.CustomTag'\n\nThese settings can be overridden per ChatBot instance using the ``statement_model`` and \n``tag_model`` kwargs on the storage adapter. See :doc:`custom-models` for details.\n\n.. code-block:: python\n   :caption: Per-instance configuration\n\n   from chatterbot import ChatBot\n\n   bot = ChatBot(\n       'My Bot',\n       storage_adapter='chatterbot.storage.DjangoStorageAdapter',\n       statement_model='myapp.CustomStatement',\n       tag_model='myapp.CustomTag'\n   )\n\nUsing a Secondary Database\n===========================\n\nChatterBot can store its data in a separate database from your main Django application. This is useful for:\n\n- **Data isolation** - Separate backups and maintenance schedules\n- **Performance** - Reduce load on your main application database\n- **Organization** - Clear separation between conversational data and application data\n- **Scalability** - Different database engines or servers for different needs\n\nConfiguration\n-------------\n\n1. Define your databases in ``settings.py``:\n\n.. code-block:: python\n   :caption: settings.py\n\n   DATABASES = {\n       'default': {\n           'ENGINE': 'django.db.backends.sqlite3',\n           'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),\n       },\n       'chatbot_db': {\n           'ENGINE': 'django.db.backends.sqlite3',\n           'NAME': os.path.join(BASE_DIR, 'chatbot.sqlite3'),\n       }\n   }\n\n2. Configure ChatterBot to use the secondary database:\n\n.. code-block:: python\n   :caption: settings.py\n\n   CHATTERBOT = {\n       'name': 'Tech Support Bot',\n       'storage_adapter': 'chatterbot.storage.DjangoStorageAdapter',\n       'database': 'chatbot_db',  # Specify the database alias\n       'logic_adapters': [\n           'chatterbot.logic.BestMatch'\n       ]\n   }\n\n3. Run migrations on the secondary database:\n\n.. code-block:: bash\n\n   python manage.py migrate --database=chatbot_db\n\nChatterBot will now utilize the secondary database to store and query its data.\n\nExample with PostgreSQL\n-----------------------\n\nYou can use different database engines for your main app and ChatterBot:\n\n.. code-block:: python\n   :caption: settings.py\n\n   DATABASES = {\n       'default': {\n           'ENGINE': 'django.db.backends.postgresql',\n           'NAME': 'myapp_db',\n           'USER': 'myuser',\n           'PASSWORD': 'mypassword',\n           'HOST': 'localhost',\n           'PORT': '5432',\n       },\n       'chatbot_db': {\n           'ENGINE': 'django.db.backends.postgresql',\n           'NAME': 'chatbot_db',\n           'USER': 'chatbot_user',\n           'PASSWORD': 'chatbot_password',\n           'HOST': 'chatbot.example.com',\n           'PORT': '5432',\n       }\n   }\n\n   CHATTERBOT = {\n       'name': 'Support Bot',\n       'storage_adapter': 'chatterbot.storage.DjangoStorageAdapter',\n       'database': 'chatbot_db',\n   }\n"
  },
  {
    "path": "docs/django/tutorial/django-filter-tutorial/index.rst",
    "content": "======================\ndjango-filter tutorial\n======================\n\nIn this section, we will be adding filtering functionality to the API built in the :ref:`Django REST Framework Tutorial`. To do this we'll be using the `django-filter` package.\n\nFor more information, ``django-filter`` has a comprehensive setup guide in its `documentation <https://django-filter.readthedocs.io/en/stable/guide/rest_framework.html>`_.\n\n1. Install django-filter\n========================\n\nTo install ``django-filter``, run the following command:\n\n.. sourcecode:: sh\n\n   pip install django-filter\n\n\n2. Configure django-filter settings\n===================================\n\nAdd ``django_filters`` to your ``INSTALLED_APPS`` in the ``settings.py`` file of your Django project.\n\n.. code-block:: python\n   :caption: tutorial/settings.py\n\n   INSTALLED_APPS = (\n       # ...\n       'django_filters',\n   )\n\nAlso add the following lines to the ``REST_FRAMEWORK`` setting in the same file:\n\n.. code-block:: python\n   :caption: tutorial/settings.py\n\n   REST_FRAMEWORK = {\n       'DEFAULT_FILTER_BACKENDS': (\n           'django_filters.rest_framework.DjangoFilterBackend',\n           # ...\n       )\n   }\n\n\n3. Configure filtering for the API\n==================================\n\nDefine a new ``filterset_fields`` variable at the class level of the viewset you created in the previous tutorial:\n\n.. code-block:: python\n    :caption: tutorial/viewsets.py\n\n    # ...\n\n    class ChapterViewSet(viewsets.ModelViewSet):\n        queryset = Chapter.objects.all()\n        serializer_class = ChapterSerializer\n\n        # Add this line:\n        filterset_fields = ('number', 'title')\n\n    # ...\n\n4. Test the filtering\n=====================\n\nYou can now test the filtering by adding query parameters to the API URL. For example, to filter chapters by title, you can use the following URL:\n\n.. code-block:: text\n\n   http://localhost:8000/api/chapters?title=Introduction&number=1\n\n\nYou'll see that only the chapter with the title \"Introduction\" and number \"1\" is returned in the response. Note that you may need to add more chapters to your database to fully see the filtering in action.\n\n5. Conclusion on django-filter\n==============================\n\nNow you know how to add filtering functionality to your APIs. The ``django-filter`` package has a number of additional features including the ability to create more complex and custom filters. For more information, see the `django-filter documentation <https://django-filter.readthedocs.io/en/stable/guide/rest_framework.html>`_.\n\nTo wrap up these tutorials, in the :ref:`next section <Writing Tests for Django Applications>` we'll be covering how to write tests for the API you just built.\n"
  },
  {
    "path": "docs/django/tutorial/django-rest-framework-tutorial/index.rst",
    "content": "==============================\nDjango REST Framework Tutorial\n==============================\n\nThis is the second portion of ChatterBot's Django tutorial. These tutorial are intended to briefly introduce beginners to the basics of Django, Django REST Framework, and other related libraries that provide a powerful foundation of knowledge for developing Python applications. For a more comprehensive tutorial, the `official documentation for Django REST Framework`_ is highly recommended.\n\nIn this section, we will be adding a REST API to our Django project from the :ref:`previous tutorial <Django Tutorial (Part 1)>`.\n\n.. _official documentation for Django REST Framework: https://www.django-rest-framework.org/tutorial/quickstart/\n\n\n1. Install Django REST Framework\n================================\n\nBegin by installing Django REST Framework with the following command:\n\n.. sourcecode:: sh\n\n   pip install djangorestframework\n\n\n2. Configure Django REST Framework settings\n===========================================\n\nNext, add ``rest_framework`` to your ``INSTALLED_APPS`` in the ``settings.py`` file of your Django project.\n\n.. code-block:: python\n   :caption: tutorial/settings.py\n\n   INSTALLED_APPS = (\n       # ...\n       'rest_framework',\n   )\n\nFinish configuring Django REST Framework for your project by adding the following lines to your ``settings.py`` file.\n\n.. code-block:: python\n   :caption: tutorial/settings.py\n\n   REST_FRAMEWORK = {\n       'DEFAULT_AUTHENTICATION_CLASSES': [\n           'rest_framework.authentication.SessionAuthentication',\n       ],\n       'DEFAULT_PERMISSION_CLASSES': [\n           'rest_framework.permissions.IsAuthenticated',\n       ],\n       'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',\n       'PAGE_SIZE': 10\n   }\n\n\n3. Create a Serializer\n======================\n\nA serializer is a class that defines the fields that get converted to JSON. Create a new file called ``serializers.py`` in the ``chapters`` directory.\n\n.. code-block:: python\n   :caption: chapters/serializers.py\n\n   from rest_framework import serializers\n   from chapters.models import Chapter\n\n\n   class ChapterSerializer(serializers.ModelSerializer):\n       class Meta:\n           model = Chapter\n           fields = (\n                'title',\n                'number',\n           )\n\n\n4. Create a ViewSet\n===================\n\nA viewset is a class that provides the actions that can be performed on a resource. Create a new file called ``viewsets.py`` in the ``chapters`` directory.\n\n.. code-block:: python\n   :caption: chapters/viewsets.py\n\n   from rest_framework import viewsets\n   from chapters.models import Chapter\n   from chapters.serializers import ChapterSerializer\n\n\n   class ChapterViewSet(viewsets.ModelViewSet):\n       queryset = Chapter.objects.all()\n       serializer_class = ChapterSerializer\n\n\n5. Add a Router\n===============\n\nA router is a class that automatically determines the URL conf for a set of views. You can add your router to your project's existing ``urls.py`` file.\n\n.. code-block:: python\n   :caption: tutorial/urls.py\n\n   from django.urls import include\n   from rest_framework.routers import DefaultRouter\n   from chapters.viewsets import ChapterViewSet\n\n   router = DefaultRouter()\n   router.register(r'chapters', ChapterViewSet)\n\n   urlpatterns = [\n       # ...\n       path('api/', include(router.urls)),\n   ]\n\n6. Test Your API\n================\n\nTo test your API, open a web browser and navigate to ``http://localhost:8000/api/chapters/``. You should see a JSON response with the chapters in your database.\n\n.. code-block:: JSON\n    :caption: localhost:8000/api/chapters/\n\n    {\n        \"count\": 1,\n        \"next\": null,\n        \"previous\": null,\n        \"results\": [\n            {\n                \"title\": \"Introduction\",\n                \"number\": 1\n            }\n        ]\n    }\n\n7. Conclusion\n=============\n\nThis concludes this mini tutorial on Django REST Framework. Now you know how to:\n\n- Add a REST API to your Django project\n- Create a serializer using Django REST Framework\n- Create a viewset and router to expose your API\n\nFor further learning, a good place to start would be how to use JavaScript to interact with your API from your webpages.\n\n:ref:`Up next <django-filter tutorial>` in this series we'll be continuing to build off of this project by adding filtering capabilities to our API using the ``django-filter`` package.\n"
  },
  {
    "path": "docs/django/tutorial/django-tutorial/index.rst",
    "content": "========================\nDjango Tutorial (Part 1)\n========================\n\nAs mentioned in the previous section, this is a small tutorial designed to introduce basic parts of Django in a way that is simple and easy to understand. For a more comprehensive explanation, see the tutorial in Django's official documentation: https://docs.djangoproject.com/en/4.2/intro/tutorial01/\n\n1. Install Django\n=================\n\nBegin by making sure that you have installed Django. For this tutorial we are going to be using the `latest stable version of Django`_, which is 4.2.\n\n.. sourcecode:: sh\n\n   pip install \"django~=4.2\"\n\n2. Create a Django Project\n==========================\n\nCreate a new Django project by running the following commands:\n\n.. sourcecode:: sh\n\n   django-admin startproject tutorial\n   cd tutorial\n   python manage.py startapp chapters\n\nThis will create a new directory called ``tutorial`` with the following structure:\n\n.. code-block:: sh\n\n   tutorial/\n       manage.py\n       tutorial/\n           __init__.py\n           settings.py\n           urls.py\n           wsgi.py\n           asgi.py\n       chapters/\n           __init__.py\n           migrations/\n               __init__.py\n           admin.py\n           apps.py\n           models.py\n           tests.py\n           views.py\n\n\nThese files are the basic structure of a Django project.\n\n3. Run the Development Server\n=============================\n\nNow we've gone through enough of the setup that you should be able to start the development server by running the following command:\n\n.. sourcecode:: sh\n\n   python manage.py runserver 0.0.0.0:8000\n\nYou should see the following output:\n\n.. code-block:: sh\n\n   Watching for file changes with StatReloader\n   Performing system checks...\n\n   System check identified no issues (0 silenced).\n   February 12, 2025 - 13:03:37\n   Django version 4.2.19, using settings 'tutorial.settings'\n   Starting development server at http://0.0.0.0:8000/\n   Quit the server with CONTROL-C.\n\n\nIn your web browser, go to ``http://localhost:8000/``. You should see a \"Welcome to Django\" page.\n\n.. image:: django-installed.png\n   :alt: ChatterBot process flow diagram\n   :align: center\n\n\n4. Edit the Django Project settings\n===================================\n\nOpen the file ``tutorial/settings.py`` in your text editor and add the ``chapters`` app to the ``INSTALLED_APPS`` list. When done it should look like this:\n\n.. code-block:: python\n   :caption: tutorial/settings.py\n\n   INSTALLED_APPS = [\n      'django.contrib.admin',\n      'django.contrib.auth',\n      'django.contrib.contenttypes',\n      'django.contrib.sessions',\n      'django.contrib.messages',\n      'django.contrib.staticfiles',\n\n      'chapters',\n   ]\n\n\n5. Concluding Part 1\n====================\n\nThis concludes part 1 of this mini Django tutorial. Now you know:\n\n- The basics of installing a python package\n- How to create a new Django project\n- The basic structure of a Django project\n- How to start the development server\n\nIn the next part of this tutorial, we will be covering concepts including views, models, and templates.\n\n:ref:`Continue to Part 2 <Django Tutorial (Part 2)>`.\n\n.. _latest stable version of Django: https://www.djangoproject.com/download/\n"
  },
  {
    "path": "docs/django/tutorial/django-tutorial/part-2.rst",
    "content": "========================\nDjango Tutorial (Part 2)\n========================\n\nIn the :ref:`previous part <Django Tutorial (Part 1)>` of this tutorial we set up our Django app. Now we're going to create a new model, and a view. If this is your first time working with these, Django's models are classes that represent database tables, and Django's views are functions that handle HTTP requests and return  HTTP responses.\n\n1. Creating a New Model\n=======================\n\nIn the ``chapters`` directory, open the file ``models.py``. This file is where you define your models. Add the following code to the file:\n\n.. code-block:: python\n    :caption: chapters/models.py\n\n    from django.db import models\n\n\n    class Chapter(models.Model):\n        \"\"\"\n        A model representing a chapter in a digital book.\n        \"\"\"\n        title = models.CharField(max_length=100)\n        number = models.IntegerField(unique=True)\n\nNext, run the following commands. The first will generate a migration file, code that tells Django how to create a new table in a database to correspond to this model. The second will run the migration, and create the table in the database.\n\n.. sourcecode:: sh\n\n    python manage.py makemigrations\n    python manage.py migrate\n\nYou should see output similar to the following:\n\n.. code-block:: sh\n\n    Migrations for 'chapters':\n      chapters/migrations/0001_initial.py\n        - Create model Chapter\n\n    Operations to perform:\n      Apply all migrations: admin, auth, chapters, contenttypes, sessions\n    Running migrations:\n      Applying contenttypes.0001_initial... OK\n      Applying auth.0001_initial... OK\n      Applying admin.0001_initial... OK\n      Applying admin.0002_logentry_remove_auto_add... OK\n      Applying admin.0003_logentry_add_action_flag_choices... OK\n      Applying contenttypes.0002_remove_content_type_name... OK\n      Applying auth.0002_alter_permission_name_max_length... OK\n      Applying auth.0003_alter_user_email_max_length... OK\n      Applying auth.0004_alter_user_username_opts... OK\n      Applying auth.0005_alter_user_last_login_null... OK\n      Applying auth.0006_require_contenttypes_0002... OK\n      Applying auth.0007_alter_validators_add_error_messages... OK\n      Applying auth.0008_alter_user_username_max_length... OK\n      Applying auth.0009_alter_user_last_name_max_length... OK\n      Applying auth.0010_alter_group_name_max_length... OK\n      Applying auth.0011_update_proxy_permissions... OK\n      Applying auth.0012_alter_user_first_name_max_length... OK\n      Applying chapters.0001_initial... OK\n      Applying sessions.0001_initial... OK\n\n\n2. Creating a New View\n======================\n\nIn Django, views let you define what content gets returned in response to a request. In this part of the tutorial, we're going to set up a view that renders an HTML page for the requested chapter and populate that page with the corresponding data using the model we previously created.\n\nStart by creating a new file and folder in the ``chapters`` directory: ``templates/chapter.html``. Add the following code to the file:\n\n.. code-block:: html\n    :caption: chapters/templates/chapter.html\n\n    <!DOCTYPE html>\n    <html>\n    <head>\n        <title>{{ chapter.title }}</title>\n    </head>\n    <body>\n        <h1>{{ chapter.title }}</h1>\n        <p>This is chapter {{ chapter.number }}.</p>\n    </body>\n    </html>\n\nIn the ``chapters`` directory, open the file ``chapters/views.py``. This file is where you define your views. Add the following code to the file:\n\n.. code-block:: python\n    :caption: chapters/views.py\n\n    from django.views.generic import TemplateView\n    from django.http import HttpResponse\n    from chapters.models import Chapter\n\n    class ChapterView(TemplateView):\n        \"\"\"\n        Render an HTML page for the requested chapter.\n        \"\"\"\n\n        template_name = 'chapter.html'\n\n        def get_context_data(self, **kwargs):\n            context = super().get_context_data(**kwargs)\n\n            context['chapter'] = Chapter.objects.get(number=kwargs['number'])\n\n            return context\n\n\nFinally, open the file ``tutorial/urls.py``. This file is where you define the URLs for your views. Ensure the following import for ``ChapterView`` is at the top of the file, and that the ``path`` for the view is included in the ``urlpatterns`` list:\n\n.. code-block:: python\n    :caption: tutorial/urls.py\n\n    from django.urls import path\n    from chapters.views import ChapterView\n\n    urlpatterns = [\n        path('chapter/<int:number>/', ChapterView.as_view(), name='chapter'),\n    ]\n\n\n3. Adding a Chapter\n===================\n\nTo add a chapter to the database, run the following command:\n\n.. sourcecode:: sh\n\n    python manage.py shell\n\n\nThis will open a Python shell. Run the following commands to create a new chapter:\n\n.. code-block:: python\n\n    from chapters.models import Chapter\n\n    Chapter.objects.create(title='Introduction', number=1)\n\n\nUse ``Ctrl-D`` or type in ``exit()`` to exit the shell.\n\nNow you should be able to visit ``http://localhost:8000/chapter/1/`` in your web browser and see the chapter page you created.\n\n4. Concluding Part 2\n====================\n\nThis concludes part 2 of this mini Django tutorial. Now you know:\n\n- How to create a new model in Django\n- How to generate and run migrations\n- How to add views with html templates\n- How to add a url for your views\n- How to add data to your database using Django's shell\n\nIf you still want to learn more, `Django's official documentation <https://docs.djangoproject.com/en/4.2/contents/>`_ is a great place to start. \n\n:ref:`Up next <Django REST Framework Tutorial>` in this series we'll be building off of the project we've begun to learn about how to add Django REST Framework to an existing Django project.\n"
  },
  {
    "path": "docs/django/tutorial/index.rst",
    "content": "================\nDjango Tutorials\n================\n\nThese are mini tutorials designed to introduce basic parts of Django in a way that is quick and brief yet easy to understand. For a more comprehensive explanation, see the tutorial in Django's official documentation: https://docs.djangoproject.com/en/4.2/intro/tutorial01/\n\nThere are three tutorials in this collection. The first one is a basic introduction to Django, and the second one builds on the first to introduce another common library used alongside Django; `Django REST framework`_. The third tutorial continues by configuring the ``django-filter`` package to filter results from the API built in part 2.\n\nDjango\n------\n\n.. toctree::\n   :maxdepth: 2\n\n   django-tutorial/index\n   django-tutorial/part-2\n\n\nDjango REST Framework\n---------------------\n\n.. toctree::\n   :maxdepth: 2\n\n   django-rest-framework-tutorial/index\n\n\n.. _Django REST framework: https://www.django-rest-framework.org/\n\n\ndjango-filter\n-------------\n\n.. toctree::\n   :maxdepth: 2\n\n   django-filter-tutorial/index\n\n\nWriting Tests\n=============\n\n.. toctree::\n   :maxdepth: 2\n\n   writing-tests\n\n\n.. _Django REST framework: https://www.django-rest-framework.org/\n"
  },
  {
    "path": "docs/django/tutorial/writing-tests.rst",
    "content": "=====================================\nWriting Tests for Django Applications\n=====================================\n\nWriting tests helps ensure the quality and stability of your code. Creating tests from the start of a project is often easier than adding them later, and writing good tests can help establish beneficial patterns that others can copy later when adding new features.\n\nThis tutorial will show you how to write tests for Django applications, specifically for Django REST framework APIs.\n\nExample API Test Cases\n=======================\n\nHere are a few examples of test cases for the API that we built as a part of the :ref:`previous tutorials <Django Tutorials>`.\n\nA few things to note:\n\n- We use Django's ``reverse`` function to generate the URL for the API endpoint. This helps ensure that the tests will continue to work even if the URL changes.\n- We use Django REST framework's ``APITestCase`` class to write the test cases. This is preferred for API tests over Django's built-in ``TestCase`` classes because it provides additional features that are useful for testing APIs.\n- The ``status`` module from Django REST framework is used to check the status codes of the responses. Although not necessary, it helps keep tests more readable.\n\n.. code-block:: python\n    :caption: chapters/tests.py\n\n    from django.urls import reverse\n    from rest_framework import status\n    from rest_framework.test import APITestCase\n    from chapters.models import Chapter\n\n\n    class ChapterListApiTests(APITestCase):\n        \"\"\"\n        Test cases for the /chapter/ endpoint.\n        \"\"\"\n\n        def setUp(self):\n            # Use Django's reverse function to generate the URL\n            self.endpoint = reverse('chapter-list')\n\n        def test_list_chapters(self):\n            \"\"\"\n            A list of chapters should be returned from the API.\n            \"\"\"\n            # Create a chapter\n            Chapter.objects.create(title='Introduction', number=1)\n\n            response = self.client.get(self.endpoint, format='json')\n\n            self.assertEqual(response.status_code, status.HTTP_200_OK)\n            self.assertEqual(response.data, {\n                'count': 1,\n                'next': None,\n                'previous': None,\n                'results': [\n                    {'title': 'Introduction', 'number': 1}\n                ]\n            })\n\n        def test_filter_chapters_by_number(self):\n            \"\"\"\n            The matching chapters should be returned from the API when filtering by number.\n            \"\"\"\n            # Create chapters\n            Chapter.objects.create(title='Introduction', number=1)\n            Chapter.objects.create(title='Prologue', number=2)\n\n            response = self.client.get(self.endpoint + '?number=1', format='json')\n\n            self.assertEqual(response.status_code, status.HTTP_200_OK)\n            self.assertEqual(response.data, {\n                'count': 1,\n                'next': None,\n                'previous': None,\n                'results': [\n                    {'title': 'Introduction', 'number': 1}\n                ]\n            })\n\n        def test_create_chapter(self):\n            \"\"\"\n            A chapter should be created when a POST request is sent to the API.\n            \"\"\"\n            response = self.client.post(self.endpoint, {\n                'title': 'Introduction', 'number': 1\n            }, format='json')\n\n            # Only one chapter should exist\n            self.assertEqual(Chapter.objects.count(), 1)\n\n            chapter = Chapter.objects.first()\n\n            self.assertEqual(response.status_code, status.HTTP_201_CREATED)\n            self.assertEqual(chapter.title, 'Introduction')\n            self.assertEqual(chapter.number, 1)\n\n\n    class ChapterDetailApiTests(APITestCase):\n        \"\"\"\n        Test cases for the /chapter/<id>/ endpoint.\n        \"\"\"\n\n        def setUp(self):\n            self.chapter = Chapter.objects.create(title='Introduction', number=1)\n            self.endpoint = reverse('chapter-detail', args=[self.chapter.id])\n\n        def test_update_chapter(self):\n            \"\"\"\n            A chapter should be updated when a PATCH request is sent to the API.\n            \"\"\"\n\n            response = self.client.patch(reverse('chapter-detail', args=[self.chapter.id]), {\n                'title': 'Introduction', 'number': 2\n            }, format='json')\n\n            # Only one chapter should exist\n            self.assertEqual(Chapter.objects.count(), 1)\n\n            chapter = Chapter.objects.first()\n\n            self.assertEqual(response.status_code, status.HTTP_200_OK)\n            self.assertEqual(chapter.number, 2)\n\n        def test_delete_chapter(self):\n            \"\"\"\n            A chapter should be deleted when a DELETE request is sent to the API.\n            \"\"\"\n            response = self.client.delete(reverse('chapter-detail', args=[self.chapter.id]))\n\n            # No chapters should exist\n            self.assertEqual(Chapter.objects.count(), 0)\n            self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)\n"
  },
  {
    "path": "docs/django/views.rst",
    "content": "=============================\nChatterBot Django Sample Code\n=============================\n\n.. note::\n\n   Looking for the full example app? Check it out on GitHub:\n   https://github.com/gunthercox/ChatterBot/tree/master/examples/django_example\n\nExample API Views\n=================\n\nChatterBot's Django example comes with an API view that demonstrates\none way to use ChatterBot to create an conversational API endpoint\nfor your application.\n\nThe endpoint expects a JSON request in the following format:\n\n.. code-block:: json\n\n   {\"text\": \"My input statement\"}\n\n\n.. literalinclude:: ../../examples/django_example/django_example/views.py\n   :caption: examples/django_example/django_example/views.py\n   :language: python\n   :pyobject: ChatterBotApiView\n\n\nExample Django Management Commands\n==================================\n\nChatterBot's Django example includes a management command that\ndemonstrates a simple example of training. This can be used as\na basis for other custom management commands used with other\n:ref:`training options <Training>`.\n\n.. literalinclude:: ../../examples/django_example/django_example/management/commands/train.py\n   :caption: examples/django_example/django_example/management/commands/train.py\n   :language: python\n"
  },
  {
    "path": "docs/django/wsgi.rst",
    "content": "Webservices\n===========\n\nHosting and serving web applications typically involves setting up a few additional components. A few common items are noted here to help you get started.\n\nEnvironments\n------------\n\nIf you want to host your Django app, you need to choose a method through\nwhich it will be hosted. There are a few free services that you can use\nto do this such as `Heroku`_ and `PythonAnyWhere`_.\n\nAnother good option is DigitalOcean, which is notoriously easy to use and offers a number of affordable hosting plans. If you're interested in trying out DigitalOcean we have a `referral link <https://m.do.co/c/c9a695f20505>`_ from them that will give you $200 in credit over 60 days.\n\nWSGI\n----\n\nA common method for serving Python web applications involves using a\nWeb Server Gateway Interface (`WSGI`_) package.\n\n`Gunicorn`_ is a great choice for a WSGI server. They have detailed\ndocumentation and installation instructions on their website.\n\nServing static files\n--------------------\n\nThere are numerous ways to host static files for your Django application.\nOne extremely easy way to do this is by using `WhiteNoise`_, a python package\ndesigned to make it possible to serve static files from just about any web application.\n\n.. _Heroku: https://heroku.com/\n.. _PythonAnyWhere: https://www.pythonanywhere.com/details/django_hosting\n.. _Gunicorn: http://gunicorn.org/\n.. _WhiteNoise: http://whitenoise.evans.io/en/stable/\n.. _WSGI: http://wsgi.readthedocs.io/en/latest/what.html\n"
  },
  {
    "path": "docs/encoding.rst",
    "content": "======================\nPython String Encoding\n======================\n\nThe Python developer community has published a great article that covers the\ndetails of unicode character processing.\n\n- Python 3: https://docs.python.org/3/howto/unicode.html\n- Python 2: https://docs.python.org/2/howto/unicode.html\n\nThe following notes are intended to help answer some common questions and issues\nthat developers frequently encounter while learning to properly work with different \ncharacter encodings in Python.\n\nDoes ChatterBot handle non-ascii characters?\n============================================\n\nChatterBot is able to handle unicode values correctly. You can pass to it\nnon-encoded data and it should be able to process it properly\n(you will need to make sure that you decode the output that is returned).\n\nBelow is one of ChatterBot's tests from `tests/test_chatbot.py`_,\nthis is just a simple check that a unicode response can be processed.\n\n.. code-block:: python\n\n   def test_get_response_unicode(self):\n       \"\"\"\n       Test the case that a unicode string is passed in.\n       \"\"\"\n       response = self.chatbot.get_response(u'سلام')\n       self.assertGreater(len(response.text), 0)\n\nThis test passes Python 3. It also verifies that\nChatterBot *can* take unicode input without issue.\n\nHow do I fix Python encoding errors?\n====================================\n\nWhen working with string type data in Python, it is possible to encounter errors\nsuch as the following.\n\n.. code-block:: text\n\n   UnicodeDecodeError: 'utf8' codec can't decode byte 0x92 in position 48: invalid start byte\n\nDepending on what your code looks like, there are a few things that you can do\nto prevent errors like this.\n\nUnicode header\n--------------\n\n.. code-block:: python\n\n   # -*- coding: utf-8 -*-\n\nWhen to use the unicode header\n++++++++++++++++++++++++++++++\n\nIf your strings use escaped unicode characters (they look like ``u'\\u00b0C'``) then\nyou do not need to add the header. If you use strings like ``'ØÆÅ'`` then you are required\nto use the header.\n\nIf you are using this header it must be the first line in your Python file.\n\nUnicode escape characters\n-------------------------\n\n.. code-block:: text\n\n   >>> print u'\\u0420\\u043e\\u0441\\u0441\\u0438\\u044f'\n   Россия\n\nWhen to use escape characters\n+++++++++++++++++++++++++++++\n\nPrefix your strings with the unicode escape character ``u'...'`` when you are\nusing escaped unicode characters.\n\nImport unicode literals from future\n-----------------------------------\n\n.. code-block:: python\n\n   from __future__ import unicode_literals\n\nWhen to import unicode literals\n+++++++++++++++++++++++++++++++\n\nUse this when you need to make sure that Python 3 code also works in Python 2.\n\nA good article on this can be found here: http://python-future.org/unicode_literals.html\n\n.. _`tests/test_chatbot.py`: https://github.com/gunthercox/ChatterBot/blob/master/tests/test_chatbot.py\n"
  },
  {
    "path": "docs/examples.rst",
    "content": "========\nExamples\n========\n\nThe following examples are available to help you get started with ChatterBot.\n\n.. note::\n   Before you run any example, you will need to install ChatterBot on your system.\n   See the :ref:`Setup guide <Installation>` for instructions.\n\nAll of these examples and more are available in the `examples <https://github.com/gunthercox/ChatterBot/tree/master/examples>`_ directory of the ChatterBot repository on GitHub. \n\nSimple Example\n==============\n\n.. literalinclude:: ../examples/basic_example.py\n   :caption: examples/basic_example.py\n   :language: python\n\nTerminal Example\n================\n\nThis example program shows how to create a simple terminal client\nthat allows you to communicate with your chat bot by typing into\nyour terminal.\n\n.. image:: _static/terminal-example.gif\n   :alt: ChatterBot terminal example running in Python console\n\n.. literalinclude:: ../examples/terminal_example.py\n   :caption: examples/terminal_example.py\n   :language: python\n\nUsing MongoDB\n=============\n\nBefore you can use ChatterBot's built in adapter for MongoDB,\nyou will need to `install MongoDB`_. Make sure MongoDB is\nrunning in your environment before you execute your program.\nTo tell ChatterBot to use this adapter, you will need to set\nthe `storage_adapter` parameter.\n\n.. code-block:: python\n\n   storage_adapter=\"chatterbot.storage.MongoDatabaseAdapter\"\n\n.. literalinclude:: ../examples/terminal_mongo_example.py\n   :caption: examples/terminal_mongo_example.py\n   :language: python\n\nTime and Mathematics Example\n============================\n\nChatterBot has natural language evaluation capabilities that\nallow it to process and evaluate mathematical and time-based\ninputs.\n\n.. literalinclude:: ../examples/math_and_time.py\n   :caption: examples/math_and_time.py\n   :language: python\n\nUsing SQL Adapter\n=================\n\nChatterBot data can be saved and retrieved from SQL databases.\n\n.. literalinclude:: ../examples/memory_sql_example.py\n   :caption: examples/memory_sql_example.py\n   :language: python\n\nRead only mode\n==============\n\nYour chat bot will learn based on each new input statement it receives.\nIf you want to disable this learning feature after your bot has been trained,\nyou can set `read_only=True` as a parameter when initializing the bot.\n\n.. code-block:: python\n\n   chatbot = ChatBot(\"Johnny Five\", read_only=True)\n\nUsing Large Language Models\n===========================\n\nSupport for large language models (LLMs) is in ChatterBot is still experimental\n(as of version 1.2.7). Notes and current usage example can be found in the\n:ref:`LLM Roadmap`.\n\nDjango and Flask\n================\n\n1. Django: A number of :ref:`example views are documented <Example API Views>`, as well as full example Django app.\n2. Flask: There is a separately maintained example `Flask project using ChatterBot <https://github.com/chamkank/flask-chatterbot>`_.\n\n.. _install MongoDB: https://docs.mongodb.com/manual/installation/\n"
  },
  {
    "path": "docs/faq.rst",
    "content": "==========================\nFrequently Asked Questions\n==========================\n\nThis document is comprised of questions that are frequently\nasked about ChatterBot and chat bots in general.\n\n.. toctree::\n   :maxdepth: 2\n\n   encoding\n\nHow do I deploy my chat bot to the web?\n---------------------------------------\n\nThere are a number of excellent web frameworks for creating\nPython projects out there. Django and Flask are two excellent\nexamples of these. ChatterBot is designed to be agnostic to\nthe platform it is deployed on and it is very easy to get set up.\n\nTo run ChatterBot inside of a web application you just need a way\nfor your application to receive incoming data and to return data.\nYou can do this any way you want, HTTP requests, web sockets, etc.\n\nThere are a number of existing examples that show how to do this.\n\n1. An example using Django: https://github.com/gunthercox/ChatterBot/tree/master/examples/django_example\n2. An example using Flask: https://github.com/chamkank/flask-chatterbot/blob/master/app.py\n\nAdditional details and recommendations for configuring Django can be found\nin the :ref:`Webservices` section of ChatterBot's Django documentation.\n\nWhat kinds of machine learning does ChatterBot use?\n---------------------------------------------------\n\nIn brief, ChatterBot uses a number of different machine learning techniques to\ngenerate its responses. The specific algorithms depend on how the chat bot is\nused and the settings that it is configured with.\n\nHere is a general overview of some of the various machine learning techniques\nthat are employed throughout ChatterBot's codebase.\n\n1. Search algorithms\n++++++++++++++++++++\n\nSearching is the most rudimentary form of artificial intelligence. To be fair,\nthere are differences between machine learning and artificial intelligence but\nlets avoid those for now and instead focus on the topic of algorithms that make\nthe chat bot talk intelligently.\n\nSearch is a crucial part of how a chat bot quickly and efficiently retrieves\nthe possible candidate statements that it can respond with.\n\nSome examples of attributes that help the chat bot select a response include\n\n- the similarity of an input statement to known statements\n- the frequency in which similar known responses occur\n- the likeliness of an input statement to fit into a category that known statements are a part of\n\n2. Classification algorithms\n++++++++++++++++++++++++++++\n\nSeveral logic adapters in ChatterBot use `naive Bayesian classification`_\nalgorithms to determine if an input statement meets a particular set of\ncriteria that warrant a response to be generated from that logic adapter.\n\n.. _naive Bayesian classification: https://en.wikipedia.org/wiki/Naive_Bayes_classifier\n"
  },
  {
    "path": "docs/filters.rst",
    "content": "=======\nFilters\n=======\n\nFilters are an efficient way to create queries that can be passed to ChatterBot's storage adapters.\nFilters will reduce the number of statements that a chat bot has to process when it is selecting a response.\n\nSetting filters\n===============\n\n.. code-block:: python\n\n   chatbot = ChatBot(\n       \"My ChatterBot\",\n       filters=[filters.get_recent_repeated_responses]\n   )\n\n.. automodule:: chatterbot.filters\n   :members:\n"
  },
  {
    "path": "docs/glossary.rst",
    "content": "========\nGlossary\n========\n\n.. glossary::\n\n   adapters\n      A base class that allows a ChatBot instance to execute some kind of functionality.\n\n   chat bot\n      Computer software that can converse conversation with human users or other chat bots [1]_.\n\n   logic adapter\n      An adapter class that allows a ChatBot instance to select a response to \n\n   RAG\n      Retrieval-Augmented Generation. A method of by which a large language model\n      can retrieve information from a database or other source of information.\n\n   storage adapter\n      A class that allows a chat bot to store information somewhere, such as a database.\n\n   corpus\n      In linguistics, a corpus (plural corpora) or text corpus is a large\n      and structured set of texts. They are used to do statistical analysis\n      and hypothesis testing, checking occurrences or validating linguistic\n      rules within a specific language territory [2]_.\n\n   large language models\n      A type of artificial intelligence model that can generate generate\n      human-like text, often trained on a significantly large corpus of\n      text data.\n\n   MCP\n      Model Context Protocol. A protocol for providing context data to a large\n      language model to enable or improve its ability to perform various tasks.\n\n   preprocessors\n      A member of a list of functions that can be used to modify text\n      input that the chat bot receives before the text is passed to\n      the logic adapter for processing.\n\n   statement\n      A single string of text representing something that can be said.\n\n   search word\n      A word that is not a stop word and has been trimmed in some way (\n      for example through stemming).\n\n   stemming\n      A process through which a word is reduced into a derivative form.\n\n   stop word\n      A common word that is often filtered out during the process of\n      analyzing text.\n\n   response\n      A single string of text that is uttered as an answer, a reply or\n      an acknowledgement to a statement.\n\n   untrained instance\n      An untrained instance of the chat bot has an empty database.\n\n   vector\n      A mathematical representation of text that can be used to calculate\n      the similarity between two pieces of text based on the distance\n      between their vectors.\n\n   vector database\n      Any database capable of storing vectors.\n\n----\n\n.. [1] https://en.wikipedia.org/wiki/Chatbot\n.. [2] https://en.wikipedia.org/wiki/Text_corpus\n"
  },
  {
    "path": "docs/index.rst",
    "content": ".. meta::\n   :description: ChatterBot documentation: Python machine learning chatbot library with semantic vector search, AI conversational dialog engine supporting multiple languages and vector databases\n   :keywords: ChatterBot, chatbot, chat, bot, natural language processing, nlp, artificial intelligence, ai, machine learning, vector database, semantic search, vector embeddings, conversational ai, python chatbot library\n\n.. container:: banner\n\n   .. image:: ../graphics/banner.png\n      :alt: ChatterBot Banner\n      :align: center\n\nAbout ChatterBot\n================\n\nChatterBot is a Python library that makes it easy to generate automated\nresponses to a user's input. ChatterBot uses a selection of machine learning\nalgorithms to produce different types of responses. This makes it easy for\ndevelopers to create chat bots and automate conversations with users.\n\n**Modern AI Capabilities** (2025):\n\n- **Semantic Vector Search**: Advanced context understanding using vector embeddings and Redis vector database\n- **Large Language Model (LLM) Integration**: Experimental support for Ollama and OpenAI models\n- **Storage-Aware Architecture**: Automatic optimization based on storage backend capabilities\n- **Multi-Language Support**: Language-independent design with spaCy integration\n\nFor more details about the ideas and concepts behind ChatterBot see the\n:ref:`process flow diagram <process_flow_diagram>`.\n\nAn example of typical input would be something like this:\n\n.. code-block:: text\n\n   user: Good morning! How are you doing?\n   bot:  I am doing very well, thank you for asking.\n   user: You're welcome.\n   bot:  Do you like hats?\n\nOriginally, ChatterBot was created as a part of the codebase for the humanoid robot `Salvius`_. As the project grew, the :code:`chatterbot` library was released as a separate open-source project.\n\nLanguage Independence\n---------------------\n\nThe language independent design of ChatterBot allows it to be trained to speak any language.\nAdditionally, the machine-learning nature of ChatterBot allows an agent instance to improve\nit's own knowledge of possible responses as it interacts with humans and other sources of informative data.\n\n.. note::\n\n   Starting in version 1.2.0 ChatterBot has started to implement some features that are\n   language specific. This change is being made to improve the quality of responses that\n   ChatterBot can generate.\n\nHow ChatterBot Works\n--------------------\n\nChatterBot is a Python library designed to make it easy to create software that can engage in conversation.\n\nAn :term:`untrained instance` of ChatterBot starts off with no knowledge of how to communicate.\nEach time a user enters a :term:`statement`, the library saves the text that they entered and the text\nthat the statement was in response to. As ChatterBot receives more input the number of responses\nthat it can reply to, and the accuracy of each response in relation to the input statement increases.\n\nThe program selects the closest matching :term:`response` by searching for the closest matching known\nstatement that matches the input, it then chooses a response from the selection of known responses\nto that statement.\n\n.. admonition:: April 2025\n\n   The dialog processing flow will be slightly different when using\n   large language models (LLMs). See the :ref:`LLM Roadmap` for more details.\n\n..  _process_flow_diagram:\n\nProcess flow diagram\n--------------------\n\n.. image:: _static/chatterbot-process-flow.svg\n   :alt: ChatterBot process flow diagram\n\nContents:\n---------\n\n.. toctree::\n   :maxdepth: 4\n\n   About <self>\n   setup\n   quickstart\n   tutorial\n   examples\n   training\n   preprocessors\n   security\n   large-language-models\n   logic/index\n   storage/index\n   filters\n   chatterbot\n   conversations\n   comparisons\n   utils\n   corpus\n   django/index\n   faq\n   commands\n   development\n   glossary\n\nReport an Issue\n---------------\n\nPlease direct all bug reports and feature requests to the project's issue\ntracker on `GitHub`_.\n\nIndices and tables\n------------------\n\n* :ref:`genindex`\n* :ref:`modindex`\n* :ref:`search`\n\nSponsors\n--------\n\nChatterBot is sponsored by:\n\n.. raw:: html\n\n   <div style=\"text-align: center;\">\n      <a href=\"https://www.testmuai.com/?utm_source=chatterbot&utm_medium=sponsor\" target=\"_blank\">\n         <img src=\"/_static/testmu-ai-white-logo.png\" style=\"vertical-align: middle;\" width=\"250\" height=\"80\" />\n      </a>\n   </div>\n\n   <p>\n      If you, or your organization is interested in sponsoring the ChatterBot project, you can do so directly via <a href=\"https://github.com/sponsors/gunthercox\">GitHub Sponsors</a> as well as reach out directly via <code>community@chatterbot.us</code> for more information and to discuss sponsorship opportunities.\n   </p>\n\n.. _GitHub: https://github.com/gunthercox/ChatterBot/issues/\n.. _Salvius: https://salvius.org\n"
  },
  {
    "path": "docs/large-language-models.rst",
    "content": "=====================\nLarge Language Models\n=====================\n\n.. warning::\n\n    Starting in ChatterBot 1.2.7 experimental support for :term:`large language models`\n    is being added. This support is not yet complete and is not yet ready for general\n    use beyond experimental purposes. The API will likely change in the future and the\n    functionality may not be fully implemented.\n\n.. warning::\n\n    **Do not deploy LLM-enabled ChatterBot applications in production without:**\n\n    * Comprehensive security review and hardening\n    * Enabling security scanning (see :doc:`security`)\n    * Additional rate limiting and abuse prevention\n    * Monitoring and alerting for security violations\n    * Understanding of OWASP Top 10 for LLM Applications\n    * Regular security audits and penetration testing\n\n    The API may change in future releases as LLM support matures.\n\n    **Security Considerations:**\n\n    LLM applications are vulnerable to prompt injection, jailbreaking, and other\n    attacks. ChatterBot provides optional security scanning via llm-guard (see\n    :doc:`security`), but this is a baseline defense and not sufficient for\n    production deployment without additional hardening.\n\n    Review the `OWASP Top 10 for LLM Applications <https://owasp.org/www-project-top-10-for-large-language-model-applications/>`_\n    before deploying any LLM-enabled application.\n\nLLM Roadmap\n===========\n\nThe following phases of development are the general roadmap for LLM support\nin ChatterBot. The goal is to provide a simple and consistent interface for\nLLM integration, and to make it easy to add support for new LLMs as they\nbecome available.\n\n.. note::\n    * Added April 1st, 2025\n    * Last updated: February 2nd, 2026\n\n**Phase 1:**\n\nSupport for local and remote LLMs.\n\n1. ☑ Support for `Ollama LLMs`_, which at the current time appear to be the easiest to set up and run on local hardware.\n2. ☑ Support for accessing LLMs that use the OpenAI API.\n\n**Phase 2:**\n\n* ☐ Streaming response support across features in ChatterBot.\n\n.. note::\n    This functionality is being skipped for now, but may be reprioritized in the future.\n    This would be more important to implement if streaming inputs, eg. text from streaming\n    audio or video sources were being supported (which is not currently something this\n    project aims to address, nor do most LLMs support).\n\n**Phase 3:**\n\nLLM integration with specific logic adapter features via MCP tool calling.\n\n* ☑ Mathematical operations :class:`~chatterbot.logic.MathematicalEvaluation` via :mod:`mathparse`\n* ☑ Date and time :class:`~chatterbot.logic.TimeLogicAdapter`\n* ☑ Unit conversion ``UnitConversion``\n\nOne of the concepts / theories here that we want to evaluate is, for example,\nthat it may be easier (and more efficient) to teach AI to use a calculator\nthan it is to teach it the rules of mathematics. Whether this is because it\nlets us use smaller LLMs that don't have a strong understanding of math, or\nbecause in general it allows us to offload processing of other complex tasks,\nthere is likely a strong use case here.\n\nBoth interestingly and conveniently, ChatterBot's existing architecture used\nfor its logic adapters is already very similar to approaches used to provide\nadditional tools to LLMs. The Model Context Protocol (:term:`MCP`) supported by a\n`range of MCP server implementations <https://github.com/punkpeye/awesome-mcp-servers?tab=readme-ov-file#what-is-mcp>`_\ncurrently seems like a strong candidate for this.\n\nPhase 3 has been implemented using the MCP tool format to allow LLMs to invoke\nlogic adapters as tools.\n\nThe implementation supports:\n\n* **Native tool calling** for models that support it (Ollama llama3.1+, mistral, qwen2.5; all OpenAI models)\n* **Prompt-based fallback** for models without native tool support using structured JSON\n* **Hybrid configurations** where LLM adapters work alongside traditional logic adapters in consensus voting\n\n.. list-table:: Comparison of ChatterBot Architectures\n   :class: table-justified\n   :header-rows: 1\n\n   * - Classic ChatterBot (Logic Adapters)\n     - LLM with MCP\n   * - .. image:: _static/dialog-processing-flow.svg\n     - .. image:: _static/dialog-processing-flow-llm.svg\n\nLLM adapters now participate in ChatterBot's consensus voting mechanism alongside\ntraditional logic adapters. This allows multiple adapters (LLM and non-LLM) to\n\"vote\" on the best response, with the highest confidence response winning.\n\nBasic LLM Configuration\n------------------------\n\n.. code-block:: python\n\n    from chatterbot import ChatBot\n\n    bot = ChatBot(\n        'My Bot',\n        logic_adapters=[\n            {\n                'import_path': 'chatterbot.logic.OllamaLogicAdapter',\n                'model': 'llama3.1',\n                'host': 'http://localhost:11434'\n            }\n        ]\n    )\n\nLLM with Tool Support\n---------------------\n\nEnable specialized tools by passing logic adapters in the ``logic_adapters_as_tools`` parameter:\n\n.. code-block:: python\n\n    bot = ChatBot(\n        'My Bot',\n        logic_adapters=[\n            {\n                'import_path': 'chatterbot.logic.OllamaLogicAdapter',\n                'model': 'llama3.1',\n                'logic_adapters_as_tools': [\n                    'chatterbot.logic.MathematicalEvaluation',\n                    'chatterbot.logic.TimeLogicAdapter',\n                    'chatterbot.logic.UnitConversion'\n                ]\n            }\n        ]\n    )\n\nWhen a tool-capable model is used (e.g., llama3.1, mistral, qwen2.5, or any OpenAI model),\nthe LLM will be able to invoke these tools using native function calling. For models without\nnative support, the adapter automatically falls back to prompt-based tool calling.\n\nHybrid Configuration\n--------------------\n\nCombine LLM adapters with traditional logic adapters for consensus voting:\n\n.. code-block:: python\n\n    bot = ChatBot(\n        'My Bot',\n        logic_adapters=[\n            {\n                'import_path': 'chatterbot.logic.OllamaLogicAdapter',\n                'model': 'llama3.1',\n                'logic_adapters_as_tools': [\n                    'chatterbot.logic.MathematicalEvaluation',\n                    'chatterbot.logic.TimeLogicAdapter'\n                ],\n                'min_confidence': 0.6,\n                'max_confidence': 0.9\n            },\n            'chatterbot.logic.BestMatch',\n            'chatterbot.logic.SpecificResponseAdapter'\n        ]\n    )\n\nIn this configuration, the LLM adapter votes alongside BestMatch and SpecificResponseAdapter,\nwith the highest confidence response being selected. The ``min_confidence`` and ``max_confidence``\nparameters control the LLM's confidence range for voting purposes.\n\n**Phase 4:**\n\n* ☐ LLM integration with the ChatterBot training process\n\nThe ideal outcome for this phase would be the ability to use the existing training\npipelines to fine-tune LLMs. It isn't clear yet if this will be possible to do with\ncommon hardware, but right now this is the goal. An alternative may be to use a RAG\napproach to allow the LLM to access the chat bot's database when generating responses.\n\nOllama Support\n==============\n\nChatterBot's experimental support for using Ollama LLMs can be tested using the following setup:\n\n1. Have Docker installed and running\n2. Install ChatterBot and the Ollama client library\n3. Use the following ``docker-compose`` file to run the Ollama server:\n\n.. code-block:: yaml\n   :caption: docker-compose.yml\n\n   services:\n\n    # NOTE: This setup is for AMD GPUs\n    ollama:\n        image: ollama/ollama:rocm\n        ports:\n        - \"11434:11434\"\n        volumes:\n        - ./.database/ollama:/root/.ollama\n        devices:\n        - /dev/kfd\n        - /dev/dri\n\nThe following commands can be used to download various Ollama models:\n\n.. code-block:: bash\n\n    docker compose up -d\n\n.. code-block:: bash\n\n    # Create a shell in the docker container\n    docker compose exec ollama bash\n\n    # Download and run the Gemma 3 model\n    ollama run gemma3:1b\n\n\n* More notes on the ``ollama`` container: https://hub.docker.com/r/ollama/ollama\n* Ollama model library: https://ollama.com/library\n\nThe following is an example of how to use the Ollama LLM in ChatterBot. Before running\nthem you will need to install the ``ollama`` client library. This can be done directly\nusing pip or by using the extra option from the ChatterBot package that includes it:\n\n.. code-block:: bash\n\n    pip install chatterbot[dev]\n\n.. literalinclude:: ../examples/ollama_example.py\n   :caption: examples/ollama_example.py\n   :language: python\n\nUsing the OpenAI client\n=======================\n\nThe following is an example of how to use the OpenAI client in ChatterBot. Before running\nthe example you will need to install the ``openai`` client library. This can be done directly\nusing pip or by using the extra option from the ChatterBot package that includes it:\n\n.. code-block:: bash\n\n    pip install chatterbot[dev] python-dotenv\n\n1. Obtain an OpenAI API key: https://platform.openai.com/settings/organization/api-keys\n2. Create a ``.env`` file to hold your API key in the parent directory from where your code is running.\n\n.. code-block:: bash\n    :caption: ../.env\n\n    OPENAI_API_KEY=\"API Key Here\"\n\n.. literalinclude:: ../examples/openai_example.py\n   :caption: examples/openai_example.py\n   :language: python\n\n\n.. _`Ollama LLMs`: https://ollama.com/library?sort=popular\n"
  },
  {
    "path": "docs/logic/create-a-logic-adapter.rst",
    "content": "============================\nCreating a new logic adapter\n============================\n\nYou can write your own logic adapters by creating a new class that\ninherits from ``LogicAdapter`` and overrides the necessary\nmethods established in the ``LogicAdapter`` base class.\n\nExample logic adapter\n=====================\n\n.. code-block:: python\n\n   from chatterbot.logic import LogicAdapter\n\n\n   class MyLogicAdapter(LogicAdapter):\n       def __init__(self, chatbot, **kwargs):\n           super().__init__(chatbot, **kwargs)\n\n       def can_process(self, statement):\n           return True\n\n       def process(self, input_statement, additional_response_selection_parameters):\n           import random\n\n           # Randomly select a confidence between 0 and 1\n           confidence = random.uniform(0, 1)\n\n           # For this example, we will just return the input as output\n           selected_statement = input_statement\n           selected_statement.confidence = confidence\n\n           return selected_statement\n\nDirectory structure\n===================\n\nIf you create your own logic adapter you will need to have it in a separate file from your chat bot.\nYour directory setup should look something like the following:\n\n.. code-block:: text\n\n   project_directory\n   ├── cool_chatbot.py\n   └── cool_adapter.py\n\nThen assuming that you have a class named ``MyLogicAdapter`` in your *cool_adapter.py* file,\nyou should be able to specify the following when you initialize your chat bot.\n\n.. code-block:: python\n\n   ChatBot(\n       # ...\n       logic_adapters=[\n           {\n               'import_path': 'cool_adapter.MyLogicAdapter'\n           }\n       ]\n   )\n\nResponding to specific input\n============================\n\nIf you want a particular logic adapter to only respond to a unique type of\ninput, the best way to do this is by overriding the ``can_process``\nmethod on your own logic adapter.\n\nHere is a simple example. Let's say that we only want this logic adapter to\ngenerate a response when the input statement starts with \"Hey Mike\". This\nway, statements such as \"Hey Mike, what time is it?\" will be processed,\nbut statements such as \"Do you know what time it is?\" will not be processed.\n\n.. code-block:: python\n\n   def can_process(self, statement):\n       if statement.text.startswith('Hey Mike'):\n           return True\n       else:\n           return False\n\nInteracting with services\n=========================\n\nIn some cases, it is useful to have a logic adapter that can interact with an external service or\napi in order to complete its task. Here is an example that demonstrates how this could be done.\nFor this example we will use a fictitious API endpoint that returns the current temperature.\n\n.. code-block:: python\n\n   def can_process(self, statement):\n       \"\"\"\n       Return true if the input statement contains\n       'what' and 'is' and 'temperature'.\n       \"\"\"\n       words = ['what', 'is', 'temperature']\n       if all(x in statement.text.split() for x in words):\n           return True\n       else:\n           return False\n\n   def process(self, input_statement, additional_response_selection_parameters):\n       from chatterbot.conversation import Statement\n       import requests\n\n       # Make a request to the temperature API\n       response = requests.get('https://temperature.example.com/current?units=celsius')\n       data = response.json()\n\n       # Let's base the confidence value on if the request was successful\n       if response.status_code == 200:\n           confidence = 1\n       else:\n           confidence = 0\n\n       temperature = data.get('temperature', 'unavailable')\n\n       response_statement = Statement(text='The current temperature is {}'.format(temperature))\n       response_statement.confidence = confidence\n\n       return response_statement\n\nProviding extra arguments\n=========================\n\nAll key word arguments that have been set in your ChatBot class's constructor\nwill also be passed to the ``__init__`` method of each logic adapter.\nThis allows you to access these variables if you need to use them in your logic adapter.\n(An API key might be an example of a parameter you would want to access here.)\n\nYou can override the ``__init__`` method on your logic adapter to store additional\ninformation passed to it by the ChatBot class.\n\n\n.. code-block:: python\n\n   class MyLogicAdapter(LogicAdapter):\n       def __init__(self, chatbot, **kwargs):\n           super().__init__(chatbot, **kwargs)\n\n           self.api_key = kwargs.get('secret_key')\n\nThe ``secret_key`` variable would then be passed to the ChatBot class as shown below.\n\n.. code-block:: python\n\n   chatbot = ChatBot(\n       # ...\n       secret_key='************************'\n    )\n"
  },
  {
    "path": "docs/logic/index.rst",
    "content": "==============\nLogic Adapters\n==============\n\nLogic adapters determine the logic for how ChatterBot selects a response to a given input statement.\n\n.. toctree::\n   :maxdepth: 1\n\n   response-selection\n   create-a-logic-adapter\n\nThe logic adapter that your bot uses can be specified by setting the ``logic_adapters`` parameter\nto the import path of the logic adapter you want to use.\n\n\n.. code-block:: python\n\n   chatbot = ChatBot(\n       \"My ChatterBot\",\n       logic_adapters=[\n           \"chatterbot.logic.BestMatch\"\n       ]\n   )\n\n\nIt is possible to enter any number of logic adapters for your bot to use.\nIf multiple adapters are used, then the bot will return the response with\nthe highest calculated confidence value. If multiple adapters return the\nsame confidence, then the adapter that is entered into the list first will\ntake priority.\n\n.. image:: ../_static/dialog-processing-flow.svg\n   :alt: ChatterBot dialog processing flow\n\n\nCommon logic adapter attributes\n=================================\n\nEach logic adapter inherits the following attributes and methods.\n\n.. autoclass:: chatterbot.logic.LogicAdapter\n   :members:\n\n\nBest Match Adapter\n==================\n\n.. autofunction:: chatterbot.logic.BestMatch\n\nThe ``BestMatch`` logic adapter selects a response based on the best known match to a given statement.\n\nHow it works\n------------\n\nThe best match adapter uses a function to compare the input statement to known statements.\nOnce it finds the closest match to the input statement, it uses another function to select one of the\nknown responses to that statement.\n\nSetting parameters\n------------------\n\n.. code-block:: python\n\n   chatbot = ChatBot(\n       \"My ChatterBot\",\n       logic_adapters=[\n           {\n               \"import_path\": \"chatterbot.logic.BestMatch\",\n               \"statement_comparison_function\": chatterbot.comparisons.LevenshteinDistance,\n               \"response_selection_method\": chatterbot.response_selection.get_first_response\n           }\n       ]\n   )\n\n.. note::\n\n   The values for ``response_selection_method`` and ``statement_comparison_function`` can be a string\n   of the path to the function, or a callable.\n\n    See the :ref:`statement-comparison` documentation for the list of functions included with ChatterBot.\n\n    See the :ref:`response-selection` documentation for the list of response selection methods included with ChatterBot.\n\n\nTime Logic Adapter\n==================\n\n.. autofunction:: chatterbot.logic.TimeLogicAdapter\n\nThe ``TimeLogicAdapter`` identifies statements in which a question about the current time is asked.\nIf a matching question is detected, then a response containing the current time is returned.\n\n.. code-block:: text\n\n   User: What time is it?\n   Bot: The current time is 4:45PM.\n\n\nMathematical Evaluation Adapter\n===============================\n\n.. autofunction:: chatterbot.logic.MathematicalEvaluation\n\nThe ``MathematicalEvaluation`` logic adapter checks a given statement to see if\nit contains a mathematical expression that can be evaluated.\nIf one exists, then it returns a response containing the result.\nThis adapter is able to handle any combination of word and numeric operators.\n\n.. code-block:: text\n\n   User: What is four plus four?\n   Bot: (4 + 4) = 8\n\n\nSpecific Response Adapter\n=========================\n\nIf the input that the chat bot receives, matches the input text specified\nfor this adapter, the specified response will be returned.\n\n.. autofunction:: chatterbot.logic.SpecificResponseAdapter\n\nSpecific response example\n-------------------------\n\n.. literalinclude:: ../../examples/specific_response_example.py\n   :language: python\n\nLow confidence response example\n-------------------------------\n\n.. literalinclude:: ../../examples/default_response_example.py\n   :language: python\n"
  },
  {
    "path": "docs/logic/response-selection.rst",
    "content": "====================================\nHow logic adapters select a response\n====================================\n\nA typical logic adapter designed to return a response to\nan input statement will use two main steps to do this.\nThe first step involves searching the database for a known\nstatement that matches or closely matches the input statement.\nOnce a match is selected, the second step involves selecting a\nknown response to the selected match. Frequently, there will\nbe a number of existing statements that are responses to the\nknown match.\n\nTo help with the selection of the response, several methods\nare built into ChatterBot for selecting a response from the\navailable options.\n\n.. _response-selection:\n\nResponse selection methods\n==========================\n\n.. automodule:: chatterbot.response_selection\n   :members:\n\nUse your own response selection method\n++++++++++++++++++++++++++++++++++++++\n\nYou can create your own response selection method and use it as long as the function takes \ntwo parameters (a statements and a list of statements). The method must return a statement.\n\n.. code-block:: python\n\n   def select_response(statement, statement_list, storage=None):\n\n       # Your selection logic\n\n       return selected_statement\n\nSetting the response selection method\n=====================================\n\nTo set the response selection method for your chat bot, you\nwill need to pass the ``response_selection_method`` parameter\nto your chat bot when you initialize it. An example of this\nis shown below.\n\n.. code-block:: python\n\n   from chatterbot import ChatBot\n   from chatterbot.response_selection import get_most_frequent_response\n\n   chatbot = ChatBot(\n       # ...\n       response_selection_method=get_most_frequent_response\n   )\n\nResponse selection in logic adapters\n====================================\n\nWhen a logic adapter is initialized, the response selection method\nparameter that was passed to it can be called using ``self.select_response``\nas shown below.\n\n.. code-block:: python\n\n   response = self.select_response(\n       input_statement,\n       list_of_response_options,\n       self.chatbot.storage\n   )\n\n\nSelecting a response from multiple logic adapters\n=================================================\n\nThe ``generate_response`` method is used to select a single response from the responses\nreturned by all of the logic adapters that the chat bot has been configured to use.\nEach response returned by the logic adapters includes a confidence score that indicates\nthe likeliness that the returned statement is a valid response to the input.\n\nResponse selection\n++++++++++++++++++\n\nThe ``generate_response`` will return the response statement that has the greatest\nconfidence score. The only exception to this is a case where multiple logic adapters\nreturn the same statement and therefore *agree* on that response.\n\nFor this example, consider a scenario where multiple logic adapters are being used.\nAssume the following results were returned by a chat bot's logic adapters.\n\n+------------+--------------+\n| Confidence | Statement    |\n+============+==============+\n| 0.2        | Good morning |\n+------------+--------------+\n| 0.5        | Good morning |\n+------------+--------------+\n| 0.7        | Good night   |\n+------------+--------------+\n\nIn this case, two of the logic adapters have generated the same result.\nWhen multiple logic adapters come to the same conclusion, that statement\nis given priority over another response with a possibly higher confidence score.\nThe fact that the multiple adapters agreed on a response is a significant\nindicator that a particular statement has a greater probability of being\na more accurate response to the input.\n\nWhen multiple adapters agree on a response, the greatest confidence score that\nwas generated for that response will be returned with it.\n"
  },
  {
    "path": "docs/packaging.rst",
    "content": "==================================\nPackaging your code for ChatterBot\n==================================\n\nThere are cases where developers may want to contribute code to ChatterBot but for\nvarious reasons it doesn't make sense or isn't possible to add the code to the\nmain ChatterBot repository on GitHub.\n\nCommon reasons that code can't be contributed include:\n\n- Licensing: It may not be possible to contribute code to ChatterBot due to a licensing restriction or a copyright.\n- Demand: There needs to be a general demand from the open source community for a particular feature so that there are developers who will want to fix and improve the feature if it requires maintenance.\n\nIn addition, all code should be well documented and thoroughly tested.\n\nPackage directory structure\n---------------------------\n\nSuppose we want to create a new logic adapter for ChatterBot and add it the\nPython Package Index (PyPI) so that other developers can install it and use it.\nWe would begin doing this by setting up a directory file the following structure.\n\n.. literalinclude:: _includes/python_module_structure.txt\n   :caption: Python Module Structure\n\nMore information on creating Python packages can be found here:\nhttps://packaging.python.org/tutorials/distributing-packages/\n\nRegister on PyPI\n================\n\nCreate an account: https://pypi.python.org/pypi?%3Aaction=register_form\n\nCreate a ``.pypirc`` configuration file.\n\n.. code-block:: bash\n   :caption: .pypirc file contents\n\n   [distutils]\n   index-servers =\n   pypi\n\n   [pypi]\n   username=my_username\n   password=my_password\n\nGenerate packages\n=================\n\n.. code-block:: bash\n\n   python -m build\n\nUpload packages\n===============\n\nThe official tool for uploading Python packages is called twine.\nYou can install twine with pip if you don't already have it installed.\n\n.. code-block:: bash\n\n   pip install twine\n\n.. code-block:: bash\n\n   twine upload dist/*\n\nInstall your package locally\n============================\n\n.. code-block:: bash\n\n   cd IronyAdapter\n   pip install . --upgrade\n\nUsing your package\n==================\n\nIf you are creating a module that ChatterBot imports from a dotted module path then you\ncan set the following in your chat bot.\n\n.. code-block:: python\n\n   chatbot = ChatBot(\n       \"My ChatBot\",\n       logic_adapters=[\n           \"irony_adapter.logic.IronyAdapter\"\n       ]\n   )\n\nTesting your code\n=================\n\n.. code-block:: python\n\n   from unittest import TestCase\n\n\n   class IronyAdapterTestCase(TestCase):\n       \"\"\"\n       Test that the irony adapter allows\n       the chat bot to understand irony.\n       \"\"\"\n\n       def test_irony(self):\n          # TODO: Implement test logic\n          self.assertTrue(response.irony)"
  },
  {
    "path": "docs/preprocessors.rst",
    "content": "=============\nPreprocessors\n=============\n\nChatterBot's :term:`preprocessors` are simple functions that modify the input statement\nthat a chat bot receives before the statement gets processed by the logic adaper.\n\nHere is an example of how to set preprocessors. The ``preprocessors``\nparameter should be a list of strings of the import paths to your preprocessors.\n\n.. code-block:: python\n\n   chatbot = ChatBot(\n       'Bob the Bot',\n       preprocessors=[\n           'chatterbot.preprocessors.clean_whitespace'\n       ]\n   )\n\nPreprocessor functions\n======================\n\nChatterBot comes with several built-in preprocessors.\n\n.. autofunction:: chatterbot.preprocessors.clean_whitespace\n\n.. autofunction:: chatterbot.preprocessors.unescape_html\n\n.. autofunction:: chatterbot.preprocessors.convert_to_ascii\n\n\nCreating new preprocessors\n==========================\n\nIt is simple to create your own preprocessors. A preprocessor is just a function\nwith a few requirements.\n\n1. It must take one parameter, a ``Statement`` instance.\n2. It must return a statement instance.\n"
  },
  {
    "path": "docs/quickstart.rst",
    "content": "=================\nQuick Start Guide\n=================\n\nThe first thing you'll need to do to get started is install ChatterBot.\n\n.. code-block:: bash\n\n   pip install chatterbot\n\nSee :ref:`Installation` for options for alternative installation methods.\n\nCreate a new chat bot\n=====================\n\n.. code-block:: python\n\n   from chatterbot import ChatBot\n   chatbot = ChatBot(\"Ron Obvious\")\n\n.. note::\n\n   The only required parameter for the `ChatBot` is a name.\n   This can be anything you want.\n\nTraining your ChatBot\n=====================\n\nAfter creating a new ChatterBot instance it is also possible to train the bot.\nTraining is a good way to ensure that the bot starts off with knowledge about\nspecific responses. The current training method takes a list of statements that\nrepresent a conversation.\nAdditional notes on training can be found in the :ref:`Training` documentation.\n\n.. note::\n\n   Training is not required but it is recommended.\n\n.. code-block:: python\n\n   from chatterbot.trainers import ListTrainer\n\n   conversation = [\n       \"Hello\",\n       \"Hi there!\",\n       \"How are you doing?\",\n       \"I'm doing great.\",\n       \"That is good to hear\",\n       \"Thank you.\",\n       \"You're welcome.\"\n   ]\n\n   trainer = ListTrainer(chatbot)\n\n   trainer.train(conversation)\n\nGet a response\n==============\n\n.. code-block:: python\n\n   response = chatbot.get_response(\"Good morning!\")\n   print(response)\n"
  },
  {
    "path": "docs/releases.rst",
    "content": "====================\nReleasing ChatterBot\n====================\n\nChatterBot follows the following rules when it comes to new versions and updates.\n\nVersioning\n==========\n\nChatterBot follows semantic versioning as a set of guidelines for release versions.\n\n- **Major** releases (2.0.0, 3.0.0, etc.) are used for large, almost\n  entirely backwards incompatible changes.\n\n- **Minor** releases (2.1.0, 2.2.0, 3.1.0, 3.2.0, etc.) are used for\n  releases that contain small, backwards incompatible changes. Known\n  backwards incompatibilities will be described in the release notes.\n\n- **Patch** releases (e.g., 2.1.1, 2.1.2, 3.0.1, 3.0.10, etc.) are used\n  for releases that contain bug fixes, features and dependency changes.\n\n\nRelease Process\n===============\n\nThe following procedure is used to finalize a new version of ChatterBot.\n\n1. We make sure that all CI tests on the master branch are passing.\n\n2. We tag the release on GitHub.\n\n3. A new package is generated from the latest version of the master branch.\n\n.. code-block:: bash\n\n   python -m build\n\n4. The Python package files are uploaded to PyPi.\n\n.. code-block:: bash\n\n   twine upload dist/*\n"
  },
  {
    "path": "docs/robots.txt",
    "content": "User-agent: *\nDisallow:\n\nSitemap: https:docs.chatterbot.us/sitemap.xml\n"
  },
  {
    "path": "docs/security.rst",
    "content": "========\nSecurity\n========\n\n**Deploying chat bots to production environments requires due diligence,\nespecially in cases where LLMs are involved. At minimum, the following\nprecautions should be considered:**\n\n* Comprehensive security review\n* Additional rate limiting and abuse prevention\n* Monitoring and alerting for security violations\n* Regular security audits and penetration testing\n* Understanding of the OWASP Top 10 for LLM Applications\n\nOverview\n========\n\nWhen using ChatterBot, you may want to add security scanning to protect\nagainst common vulnerabilities outlined in the `OWASP Top 10 for LLM Applications \n<https://owasp.org/www-project-top-10-for-large-language-model-applications/>`_.\n\nChatterBot does not include built-in security scanning. Instead, you can integrate\nthird-party security tools like `llm-guard <https://protectai.github.io/llm-guard/>`_,\n`Prompt-Guard`_, or other scanning solution at the application level to scan inputs\nbefore they reach the chatbot and outputs before they are shown to users.\n\n**Depending on your use case, the following are examples of best practices you might consider:**\n\n1. **Always scan user input** for prompt injection\n2. **Always scan bot output** scan bot output to prevent PII leakage\n3. **Start with strict thresholds** and relax if false positives occur\n4. **Log security violations** for monitoring and analysis\n5. **Test with adversarial inputs** before deployment\n6. **Implement rate limiting** at application layer\n7. **Never execute LLM outputs** as code without validation\n8. **Review OWASP LLM Top 10** regularly\n\nAdditional Resources\n====================\n\n* `llm-guard Documentation <https://protectai.github.io/llm-guard/>`_\n* `OWASP Top 10 for LLM Applications <https://owasp.org/www-project-top-10-for-large-language-model-applications/>`_\n* `ProtectAI GitHub <https://github.com/protectai/llm-guard>`_\n* `ChatterBot LLM Documentation <large-language-models.html>`_\n* `Prompt-Guard <https://huggingface.co/meta-llama/Prompt-Guard-86M>`_\n"
  },
  {
    "path": "docs/setup.rst",
    "content": "============\nInstallation\n============\n\nThe recommended method for installing ChatterBot is by using `pip`_.\n\nInstalling from PyPi\n--------------------\n\nIf you are just getting started with ChatterBot, it is recommended that you\nstart by installing the latest version from the Python Package Index (`PyPi`_).\nTo install ChatterBot from PyPi using pip run the following command in your terminal.\n\n.. code-block:: bash\n\n   pip install chatterbot\n\n\nOptional dependencies\n---------------------\n\nChatterBot offers two collections of optional dependencies: ``dev`` and ``test``. Neither of these are required for all ChatterBot use cases, but both provide full support for additional features. The ``dev`` collection includes dependencies such as ``pymongo``, and ``pint`` (which are useful for working on various changes during development but are not required to use all chatterbot features). Separately the ``test`` collection includes dependencies such as ``flake8``, and ``coverage``. The specifics of each collection of optional dependencies can be reviewed via the project's `pyproject.yml`_ file. To install these optional dependencies, you can use the following commands.\n\n.. code-block:: bash\n\n   pip install chatterbot[dev]\n\n\n.. code-block:: bash\n\n   pip install chatterbot[test]\n\n\n.. code-block:: bash\n\n   pip install chatterbot[dev,test]\n\n\nSimilarly, if you have `cloned the repository <#installing-from-source>`_ and want to install the optional dependencies, you can run commands in the following format:\n\n.. code-block:: bash\n\n   pip install .[dev,test]\n\n\nInstalling from GitHub\n----------------------\n\nYou can install the latest **development** version of ChatterBot directly from GitHub using ``pip``.\n\n.. code-block:: bash\n\n   pip install git+git://github.com/gunthercox/ChatterBot.git@master\n\n\nInstalling from source\n----------------------\n\n1. Download a copy of the code from GitHub. You may need to install `git`_.\n\n.. code-block:: bash\n\n   git clone https://github.com/gunthercox/ChatterBot.git\n\n2. Install the code you have just downloaded using pip\n\n.. code-block:: bash\n\n   pip install ./ChatterBot\n\n\nChecking the version of ChatterBot that you have installed\n==========================================================\n\nIf you already have ChatterBot installed and you want to check what version you\nhave installed you can run the following command.\n\n.. code-block:: bash\n\n    python -m chatterbot --version\n\nUpgrading ChatterBot to the latest version\n==========================================\n\n.. toctree::\n   :maxdepth: 4\n\n   upgrading\n\n.. _git: https://git-scm.com/book/en/v2/Getting-Started-Installing-Git\n.. _pip: https://pip.pypa.io/en/stable/installing/\n.. _PyPi: https://pypi.python.org/pypi\n.. _pyproject.yml: https://github.com/gunthercox/ChatterBot/blob/master/pyproject.toml"
  },
  {
    "path": "docs/statements.txt",
    "content": ""
  },
  {
    "path": "docs/storage/create-a-storage-adapter.rst",
    "content": "Creating a new storage adapter\n==============================\n\nYou can write your own storage adapters by creating a new class that\ninherits from ``StorageAdapter`` and overrides necessary\nmethods established in the base ``StorageAdapter`` class.\n\nYou will then need to implement the interface established by the ``StorageAdapter`` class.\n\n.. literalinclude:: ../../chatterbot/storage/storage_adapter.py\n   :language: python\n"
  },
  {
    "path": "docs/storage/index.rst",
    "content": "================\nStorage Adapters\n================\n\n.. meta::\n   :description: ChatterBot storage adapters: SQL, Redis vector database, MongoDB. Semantic search with vector embeddings for AI-powered contextual responses\n   :keywords: storage adapter, database, SQL, Redis, MongoDB, vector database, semantic search, vector embeddings\n\nStorage adapters provide an interface that allows ChatterBot\nto connect to different storage technologies. Each adapter is optimized\nfor different use cases:\n\n- **Redis Vector Storage**: Semantic similarity search using vector embeddings (best for contextual AI responses)\n- **SQL Storage**: Traditional pattern matching with POS-lemma indexing (best for exact phrase matching)\n- **MongoDB Storage**: NoSQL document storage with flexible schema\n- **Django Storage**: Integrated with Django ORM for web applications\n\nThe storage adapter that your bot uses can be specified by setting\nthe ``storage_adapter`` parameter to the import path of the\nstorage adapter you want to use. \n\n.. code-block:: python\n\n   chatbot = ChatBot(\n       \"My ChatterBot\",\n       storage_adapter=\"chatterbot.storage.SQLStorageAdapter\"\n   )\n\nBuilt-in Storage Adapters\n=========================\n\nChatterBot includes multiple storage adapters for different AI and database technologies:\n\n.. toctree::\n   :maxdepth: 2\n\n   redis\n   mongodb\n   sql\n   ../django/index\n\nChoosing a Storage Adapter\n===========================\n\n**For Semantic AI Chatbots** (Recommended for modern conversational AI):\n\nNote that as of December 2025, the Redis Vector Storage Adapter is still an experimental beta feature.\n\nUse **Redis Vector Storage** when you need:\n\n- Context-aware responses based on meaning, not keywords\n- Vector embeddings for semantic similarity search\n- Automatic confidence scoring from cosine similarity\n- Best match for conversational AI and natural language understanding\n\n**For Pattern-Based Matching**:\n\nUse **SQL Storage** when you need:\n\n- Exact phrase or pattern matching\n- POS-lemma bigram indexing\n- Traditional database features (ACID compliance)\n- Lower memory footprint\n\n**For Flexibility**:\n\nUse **MongoDB** or **Django Storage** for schema flexibility and web framework integration.\n\nCommon storage adapter attributes\n=================================\n\nEach storage adapter inherits the following attributes and methods.\n\n.. autoclass:: chatterbot.storage.StorageAdapter\n   :members:\n\nDatabase Migrations\n===================\n\nVarious frameworks such as Django and SQL Alchemy support\nfunctionality that allows revisions to be made to databases\nprogrammatically. This makes it possible for updates and\nrevisions to structures in the database to be be applied\nin consecutive version releases.\n\nThe following explains the included migration process for\neach of the databases that ChatterBot comes with support for.\n\n* Django: Full schema migrations and data migrations will\n  be included with each release.\n* SQL Alchemy: No migrations are currently provided in\n  releases. If you require migrations between versions\n  `Alembic`_ is the recommended solution for generating them.\n* MongoDB: No migrations are provided.\n* Redis: No migrations are provided.\n\nFurther Reading\n===============\n\n.. toctree::\n   :maxdepth: 2\n\n   text-search\n   create-a-storage-adapter\n\n.. _Alembic: https://alembic.sqlalchemy.org\n"
  },
  {
    "path": "docs/storage/mongodb.rst",
    "content": "MongoDB Storage Adapter\n=======================\n\n.. image:: /_static/MongoDB_Fores-Green.svg\n   :alt: MongoDB Logo\n   :align: center\n..\n   Imaged used in accordance with the MongoDB Trademark Usage Guidelines\n   https://www.mongodb.com/legal/trademark-usage-guidelines\n\nChatterBot includes support for integration with MongoDB databases via its ``MongoDatabaseAdapter`` class.\n\nBefore you can use this storage adapter you will need to install `pymongo`_. An easy way to install it is to use the ``chatterbot[mongodb]`` extra when installing ChatterBot. For example:\n\n.. code-block:: bash\n\n   pip install chatterbot[mongodb]\n\nYou'll also need to have a MongoDB server running. An easy way to run one locally is to use Docker:\n\n.. code-block:: yaml\n   :caption: docker-compose.yml\n\n   services:\n     mongo:\n       # Use the latest stable version of the mongo image\n       image: mongo:8.0\n       # Expose the default MongoDB port\n       ports:\n         - \"27017:27017\"\n       # Persist the MongoDB data\n       volumes:\n         - ./.database/mongodb/db:/data/db\n\nTo start the MongoDB container, run:\n\n.. code-block:: bash\n\n   docker compose up -d\n\n.. note::\n\n   For more information on Docker and ``docker compose``, see the `Docker Compose documentation`_.\n\nUsing MongoDB with SSL/TLS\n--------------------------\n\nFor secure connections to remote MongoDB instances (such as Amazon DocumentDB, MongoDB Atlas, or production deployments), you can use SSL/TLS certificates.\n\nAmazon DocumentDB Example\n~~~~~~~~~~~~~~~~~~~~~~~~~\n\nAmazon DocumentDB requires SSL/TLS connections with a certificate file. Here's how to configure ChatterBot:\n\n1. Download the Amazon RDS CA certificate bundle:\n\n.. code-block:: bash\n\n   wget https://truststore.pki.rds.amazonaws.com/global/global-bundle.pem\n\n2. Configure ChatterBot to use the certificate:\n\n.. code-block:: python\n\n   from chatterbot import ChatBot\n\n   bot = ChatBot(\n       'MyBot',\n       storage_adapter='chatterbot.storage.MongoDatabaseAdapter',\n       database_uri='mongodb://USERNAME:PASSWORD@my-cluster.us-east-1.docdb.amazonaws.com:27017/?ssl=true&replicaSet=rs0&readPreference=secondaryPreferred',\n       mongodb_client_kwargs={\n           'tlsCAFile': 'global-bundle.pem'  # Path to your certificate file\n       }\n   )\n\nMongoDB Atlas Example\n~~~~~~~~~~~~~~~~~~~~~\n\nFor MongoDB Atlas with SSL/TLS:\n\n.. code-block:: python\n\n   from chatterbot import ChatBot\n\n   bot = ChatBot(\n       'MyBot',\n       storage_adapter='chatterbot.storage.MongoDatabaseAdapter',\n       database_uri='mongodb+srv://USERNAME:PASSWORD@cluster.mongodb.net/?retryWrites=true&w=majority',\n       mongodb_client_kwargs={\n           'tls': True,\n           'tlsAllowInvalidCertificates': False  # Use True only for testing\n       }\n   )\n\nSelf-Signed Certificates\n~~~~~~~~~~~~~~~~~~~~~~~~\n\nIf you're using self-signed certificates:\n\n.. code-block:: python\n\n   from chatterbot import ChatBot\n\n   bot = ChatBot(\n       'MyBot',\n       storage_adapter='chatterbot.storage.MongoDatabaseAdapter',\n       database_uri='mongodb://localhost:27017/chatterbot-database?ssl=true',\n       mongodb_client_kwargs={\n           'tlsCAFile': '/path/to/ca.pem',\n           'tlsCertificateKeyFile': '/path/to/client.pem',\n           'tlsAllowInvalidCertificates': False\n       }\n   )\n\nAdditional MongoDB Client Options\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\nThe ``mongodb_client_kwargs`` parameter accepts any valid PyMongo MongoClient options, including:\n\n- ``tlsCAFile``: Path to CA certificate file\n- ``tlsCertificateKeyFile``: Path to client certificate file\n- ``tls``: Enable/disable TLS\n- ``tlsAllowInvalidCertificates``: Allow invalid certificates (not recommended for production)\n- ``serverSelectionTimeoutMS``: Timeout for server selection\n- ``connectTimeoutMS``: Connection timeout\n- ``socketTimeoutMS``: Socket timeout\n- ``maxPoolSize``: Maximum connection pool size\n- ``minPoolSize``: Minimum connection pool size\n\nFor a complete list of options, see the `PyMongo MongoClient documentation`_.\n\nMongoDB Adapter Class Attributes\n--------------------------------\n\n.. autoclass:: chatterbot.storage.MongoDatabaseAdapter\n   :members:\n\n.. _pymongo: https://pypi.org/project/pymongo/\n.. _Docker Compose documentation: https://docs.docker.com/compose/\n.. _PyMongo MongoClient documentation: https://pymongo.readthedocs.io/en/stable/api/pymongo/mongo_client.html\n"
  },
  {
    "path": "docs/storage/redis.rst",
    "content": "Redis Vector Storage Adapter\n============================\n\n.. note::\n\n   **(December, 2025)**:\n   The ``RedisVectorStorageAdapter`` is new and experimental functionality introduced as a \"beta\" feature. Its functionality might not yet be fully stable and is subject to change in future releases.\n\n.. meta::\n   :description: Redis vector storage for ChatterBot: semantic similarity search, vector embeddings, AI-powered contextual responses with HuggingFace transformers\n   :keywords: redis vector database, semantic search, vector embeddings, sentence transformers, AI chatbot, natural language understanding, context-aware responses, vector similarity\n\n.. image:: /_static/Redis_Logo_Red_RGB.svg\n   :alt: Redis Logo\n   :align: center\n   :width: 200\n..\n    Imaged used in accordance with the Redis Trademark Policy\n    https://redis.io/legal/trademark-policy/\n\nThe ``RedisVectorStorageAdapter`` enables advanced **semantic similarity search** for ChatterBot using Redis® as a :term:`vector database`.\nUnlike traditional keyword-based storage adapters, this adapter uses **vector embeddings** and **cosine similarity** to understand conversational context and find semantically related responses.\n\n**Key Features:**\n\n- **Semantic Understanding**: Matches responses based on meaning, not just keywords\n- **Vector Embeddings**: Uses HuggingFace ``sentence-transformers/all-mpnet-base-v2`` model for state-of-the-art text encoding\n- **Confidence Scoring**: Returns similarity scores (0.0-1.0) based on vector distance for intelligent response ranking\n- **Performance Optimized**: Automatic NoOpTagger eliminates unnecessary spaCy processing overhead\n- **Context-Aware Responses**: Finds conversationally appropriate responses even when exact words differ\n\nVectors are mathematical representations of text (multi-dimensional embeddings) that capture semantic meaning.\nThe adapter calculates similarity between text by measuring the **cosine distance** between their vector representations,\nallowing ChatterBot to understand that \"How are you?\" is similar to \"How's it going?\" even with different words.\n\nFor example, consider the following words:\n\n.. code-block:: text\n\n            (Speaking)\n                ●\n               / \\\n              /   \\\n    (Poetry) ●-----● (Rhyming)\n              \\   /\n               \\ /\n                ●\n            (Writing)\n\nThe acts of \"speaking\" and \"writing\" are both forms of communication, so they are included in the same cluster, but they are somewhat opposite to each other. Both \"poetry\" and \"rhyming\" closely related, and in some cases might possibly be used as synonyms within the context of either types of speech or types of writing.\n\nRedis Setup\n-----------\n\nBefore you use the ``RedisVectorStorageAdapter`` you will need to install\nthe dependencies required for `Redis`_ and generating vectors.\nThis can be done using the ``chatterbot[redis]`` extra when\ninstalling ChatterBot. For example:\n\n.. code-block:: bash\n\n   pip install chatterbot[redis]\n\nYou will also need to have a Redis server running, with the additional\nmodules installed that enable searching using vectors. And easy way to\nrun one locally is to use Docker:\n\n.. code-block:: yaml\n   :caption: docker-compose.yml\n\n   services:\n     redis:\n       # Use the latest version of the redis-stack image\n       image: redis/redis-stack-server:latest\n       # Expose the default Redis port\n       ports:\n         - \"6379:6379\"\n       # Persist the Redis data\n       volumes:\n         - ./.database/redis/:/data\n\nTo start the Redis container, run:\n\n.. code-block:: bash\n\n   docker compose up -d\n\nLikewise, you can run ``docker compose ps`` to review the status of your container, and ``docker compose down`` to stop it. For more information on Docker and ``docker compose``, see the `Docker Compose documentation`_.\n\nRedis Configuration\n-------------------\n\nTo use the ``RedisVectorStorageAdapter`` you will need to provide the following argument when configuring your ChatterBot instance:\n\n.. code-block:: python\n\n   from chatterbot import ChatBot\n\n   chatbot = ChatBot(\n         'Redis Bot',\n         storage_adapter='chatterbot.storage.RedisVectorStorageAdapter',\n         # Optional: Override the default Redis URI\n         # database_uri='redis://localhost:6379/0'\n   )\n\nStorage-Aware Architecture\n^^^^^^^^^^^^^^^^^^^^^^^^^^\n\nThe Redis adapter automatically configures ChatterBot for optimal performance with vector-based search:\n\n- **Automatic Tagger Selection**: Uses ``NoOpTagger`` instead of ``PosLemmaTagger`` to eliminate spaCy model loading overhead\n- **Semantic Vector Search**: Automatically selects ``SemanticVectorSearch`` algorithm instead of text-based comparison\n- **No Manual Configuration**: These optimizations are applied automatically when using the Redis adapter\n\nThis \"storage-aware\" design means ChatterBot adapts its processing pipeline based on the storage adapter's capabilities,\nensuring maximum performance and accuracy for vector-based semantic search.\n\n.. code-block:: python\n\n   # No need to specify tagger or search algorithm - Redis adapter handles it!\n   chatbot = ChatBot(\n       'Semantic Bot',\n       storage_adapter='chatterbot.storage.RedisVectorStorageAdapter'\n   )\n   # Automatically uses:\n   # - NoOpTagger (no spaCy overhead)\n   # - SemanticVectorSearch (vector similarity)\n   # - Confidence scores from cosine similarity\n\nSemantic Search vs. Traditional Text Search\n-------------------------------------------\n\nThe Redis adapter uses **semantic vector search** instead of traditional pattern matching:\n\n.. list-table:: Comparison: Semantic Vector Search vs. Text-Based Search\n   :widths: 30 35 35\n   :header-rows: 1\n\n   * - Feature\n     - Traditional Text Search (SQL)\n     - Semantic Vector Search (Redis)\n   * - Search Method\n     - POS-lemma bigram matching\n     - 768-dimensional vector similarity\n   * - Context Understanding\n     - Structural patterns only\n     - Deep semantic meaning\n   * - \"How are you?\" matches \"How's it going?\"\n     - ❌ No (different lemmas)\n     - ✅ Yes (similar meaning)\n   * - Confidence Scoring\n     - Levenshtein distance\n     - Cosine similarity (1 - distance/2)\n   * - Processing Overhead\n     - Requires spaCy models\n     - No spaCy needed (NoOpTagger)\n   * - Best For\n     - Exact pattern matching\n     - Conversational AI, context understanding\n\n**Example: Semantic Similarity in Action**\n\n.. code-block:: python\n\n   # These inputs find similar responses despite different words:\n   response1 = chatbot.get_response(\"What's the weather like?\")\n   response2 = chatbot.get_response(\"How's the climate today?\")\n   # Both queries find weather-related responses due to semantic similarity\n\n   # Confidence scores help rank responses:\n   # - Vector distance 0.1 → confidence ~0.95 (very similar)\n   # - Vector distance 0.5 → confidence ~0.75 (somewhat similar)\n   # - Vector distance 1.0 → confidence ~0.50 (loosely related)\n\nClass Attributes\n----------------\n\n.. autoclass:: chatterbot.storage.RedisVectorStorageAdapter\n   :members:\n\nPerformance Considerations\n--------------------------\n\n**Vector Embedding Model**: By default, the Redis adapter uses ``sentence-transformers/all-mpnet-base-v2`` from HuggingFace:\n\n- **Dimensions**: 768-dimensional embeddings\n- **Model Size**: ~420MB (downloaded once, cached locally)\n- **Performance**: ~2000 sentences/second on CPU\n- **Quality**: State-of-the-art semantic similarity (as of 2025)\n\n**First-Time Setup**: The embedding model downloads automatically on first use:\n\n.. code-block:: python\n\n   # First initialization downloads model (~420MB)\n   chatbot = ChatBot('Bot', storage_adapter='chatterbot.storage.RedisVectorStorageAdapter')\n   # Subsequent uses load from cache (fast startup)\n\n**Memory Usage**: Redis vector storage requires more memory than SQL due to embedding storage:\n\n- Each statement: ~3KB (768 floats × 4 bytes)\n- 10,000 statements: ~30MB vector data\n- Trade-off: Higher memory for better semantic understanding\n\nEmbedding Model Configuration\n------------------------------\n\n.. versionadded:: 1.2.8\n   Support for configurable embedding models and providers.\n\nThe Redis adapter now supports custom embedding models and providers, allowing you to optimize for different use cases:\n\nDefault Configuration\n^^^^^^^^^^^^^^^^^^^^^\n\nBy default, the adapter uses HuggingFace's ``sentence-transformers/all-mpnet-base-v2``:\n\n.. code-block:: python\n\n   chatbot = ChatBot(\n       'Bot',\n       storage_adapter='chatterbot.storage.RedisVectorStorageAdapter'\n   )\n   # Uses: sentence-transformers/all-mpnet-base-v2 (768-dim, balanced)\n\nAlternative HuggingFace Models\n^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\nChoose different models based on your requirements:\n\n**Fast/Lightweight Model** (for high-throughput applications):\n\n.. code-block:: python\n\n   chatbot = ChatBot(\n       'FastBot',\n       storage_adapter='chatterbot.storage.RedisVectorStorageAdapter',\n       embedding_model='all-MiniLM-L6-v2'\n   )\n   # Model: all-MiniLM-L6-v2\n   # Dimensions: 384 (vs 768 default)\n   # Size: 80MB (vs 420MB default)\n   # Speed: 5x faster\n   # Quality: ~10% lower accuracy\n\n**Q&A Optimized Model** (for question-answering chatbots):\n\n.. code-block:: python\n\n   chatbot = ChatBot(\n       'QABot',\n       storage_adapter='chatterbot.storage.RedisVectorStorageAdapter',\n       embedding_model='multi-qa-mpnet-base-dot-v1'\n   )\n   # Model: multi-qa-mpnet-base-dot-v1\n   # Trained specifically on Q&A datasets\n   # Better performance on question-answer pairs\n\n**Multilingual Model** (for multi-language support):\n\n.. code-block:: python\n\n   chatbot = ChatBot(\n       'MultilingualBot',\n       storage_adapter='chatterbot.storage.RedisVectorStorageAdapter',\n       embedding_model='paraphrase-multilingual-mpnet-base-v2'\n   )\n   # Model: paraphrase-multilingual-mpnet-base-v2\n   # Supports 50+ languages\n   # Same 768 dimensions as default\n\nAdvanced Embedding Configuration\n^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\nPass additional parameters to the embedding model:\n\n.. code-block:: python\n\n   chatbot = ChatBot(\n       'CustomBot',\n       storage_adapter='chatterbot.storage.RedisVectorStorageAdapter',\n       embedding_model='all-MiniLM-L6-v2',\n       embedding_kwargs={\n           'model_kwargs': {\n               'device': 'cpu',  # or 'cuda' for GPU\n               'torch_dtype': 'float16'  # Reduce memory usage\n           },\n           'encode_kwargs': {\n               'normalize_embeddings': True,  # L2 normalization\n               'batch_size': 32  # Process 32 texts at once\n           }\n       }\n   )\n\nAlternative Embedding Providers\n^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\n**OpenAI Embeddings** (cloud-based, requires API key):\n\n.. code-block:: python\n\n   # Requires: pip install langchain-openai\n   # Set: export OPENAI_API_KEY=\"your-api-key\"\n\n   chatbot = ChatBot(\n       'OpenAIBot',\n       storage_adapter='chatterbot.storage.RedisVectorStorageAdapter',\n       embedding_provider='openai',\n       embedding_model='text-embedding-3-small',\n       embedding_kwargs={'dimensions': 1536}\n   )\n   # Pros: High quality, fast API calls\n   # Cons: Costs money per token, requires internet\n\n**Cohere Embeddings** (cloud-based, requires API key):\n\n.. code-block:: python\n\n   # Requires: pip install langchain-cohere\n   # Set: export COHERE_API_KEY=\"your-api-key\"\n\n   chatbot = ChatBot(\n       'CohereBot',\n       storage_adapter='chatterbot.storage.RedisVectorStorageAdapter',\n       embedding_provider='cohere',\n       embedding_model='embed-english-v3.0'\n   )\n   # Pros: Optimized for semantic search\n   # Cons: Subscription-based, requires internet\n\nModel Selection Guide\n^^^^^^^^^^^^^^^^^^^^^\n\n.. list-table:: Embedding Model Comparison\n   :widths: 25 20 20 35\n   :header-rows: 1\n\n   * - Use Case\n     - Recommended Model\n     - Dimensions\n     - Trade-offs\n   * - General chatbot (default)\n     - ``all-mpnet-base-v2``\n     - 768\n     - Best balance of speed and quality\n   * - High-throughput / limited resources\n     - ``all-MiniLM-L6-v2``\n     - 384\n     - 5x faster, smaller size, slight quality loss\n   * - Question-answering bot\n     - ``multi-qa-mpnet-base-dot-v1``\n     - 768\n     - Better Q&A performance, same size as default\n   * - Multilingual bot\n     - ``paraphrase-multilingual-mpnet-base-v2``\n     - 768\n     - Supports 50+ languages, same size\n   * - Cloud/Production (API)\n     - OpenAI or Cohere\n     - 1024-1536\n     - Higher quality, costs money, requires API key\n\n**Example: Complete Configuration**\n\nSee ``examples/redis_embedding_examples.py`` for complete working examples of all embedding configurations.\n\nMore on Vector Databases & Semantic Search\n-------------------------------------------\n\nFor those looking to learn more about vector databases, vector embeddings, and semantic search in AI applications:\n\n.. list-table:: Vector Database & AI Learning Resources\n   :widths: 50 50\n   :header-rows: 1\n\n   * - Topic\n     - Resource Link\n   * - What is a vector database?\n     - https://www.mongodb.com/resources/basics/databases/vector-databases\n   * - Why use a vector database?\n     - https://stackoverflow.blog/2023/09/20/do-you-need-a-specialized-vector-database-to-implement-vector-search-well/\n   * - How to choose a vector database?\n     - https://learn.microsoft.com/en-us/azure/cosmos-db/mongodb/vcore/vector-search-ai\n   * - Redis as a vector database\n     - https://redis.io/docs/latest/develop/interact/search-and-query/advanced-concepts/vectors/\n   * - Sentence Transformers (Embeddings)\n     - https://www.sbert.net/\n   * - Understanding Cosine Similarity\n     - https://en.wikipedia.org/wiki/Cosine_similarity\n   * - Vector Search for AI/LLMs\n     - https://www.pinecone.io/learn/vector-search-basics/\n\n\n:sub:`* Redis is a registered trademark of Redis Ltd. Any rights therein are reserved to Redis Ltd.`\n\n\n.. _Redis: https://redis.io/docs/latest/operate/oss_and_stack/install/install-redis/\n.. _Docker Compose documentation: https://docs.docker.com/compose/\n"
  },
  {
    "path": "docs/storage/sql.rst",
    "content": "SQL Storage Adapter\n===================\n\n.. autoclass:: chatterbot.storage.SQLStorageAdapter\n   :members:\n"
  },
  {
    "path": "docs/storage/text-search.rst",
    "content": "===========\nText Search\n===========\n\nChatterBot's storage adapters support text search functionality.\n\nText Search Example\n===================\n\n.. literalinclude:: ../../tests/test_chatbot.py\n   :language: python\n   :pyobject: ChatterBotResponseTestCase.test_search_text_results_after_training\n\nBigram Text Index\n=================\n\nBigram pairs are used for text search \n\nIn addition, the generation of the pairs ensures that there is a smaller number\nof possible matches based on the probability of finding two neighboring words\nin an existing string that match the search parameter.\n\nFor searches in larger data sets, the bigrams also reduce the number of ``OR``\ncomparisons that need to occur on a database level. This will always be a\nreduction of ``n - 1`` where ``n`` is the number of search words.\n\n.. image:: ../_static/bigrams.svg\n   :alt: ChatterBot bigram generation process\n"
  },
  {
    "path": "docs/testing.rst",
    "content": "============\nUnit Testing\n============\n\n*\"A true professional does not waste the time and money of other people by handing over software that is not reasonably free of obvious bugs;\nthat has not undergone minimal unit testing; that does not meet the specifications and requirements;\nthat is gold-plated with unnecessary features; or that looks like junk.\"* – Daniel Read\n\nRunning tests\n-------------\n\nYou can run ChatterBot's main test suite using Python's built-in test runner. For example:\n\n.. sourcecode:: sh\n\n   python -m unittest discover -s tests -v\n\nThis command will run all tests including Django integration tests (if Django is installed).\n\n*Note* that the ``unittest`` command also allows you to specify individual test cases to run.\nFor example, the following command will run all tests in the test-module `tests/logic/`\n\n.. sourcecode:: sh\n\n   python -m unittest discover -s tests/logic/ -v\n\nTo run a specific test in a test class you can specify the test method name using the following pattern:\n\n.. sourcecode:: sh\n\n   python -m unittest tests.logic.test_best_match.BestMatchTestCase.test_match_with_response\n\nTests can also be run in \"fail fast\" mode, in which case they will run until the first test failure is encountered.\n\n.. sourcecode:: sh\n\n   python -m unittest discover -f tests\n\nDjango integration tests\n------------------------\n\nDjango integration tests are included in ``tests/django_integration/`` and will automatically run \nwhen you execute the main test suite (if Django is installed). If Django is not available, \nthese tests will be gracefully skipped.\n\nTo run only Django integration tests:\n\n.. sourcecode:: sh\n\n   python -m unittest discover -s tests/django_integration/ -v\n\nThe Django example app tests can be run separately with the following command from within \nthe `examples/django_example` directory:\n\n.. sourcecode:: sh\n\n   python manage.py test\n\nBenchmark tests\n---------------\n\nYou can run a series of benchmark tests that test a variety of different chat bot configurations for\nperformance by running the following command.\n\n.. sourcecode:: sh\n\n   python tests/benchmarks.py\n\n\nTesting documentation builds\n----------------------------\n\nThe HTML documentation for ChatterBot can be compiled using using `Sphinx`_. To build it run the following command from the root directory of the project:\n\n.. sourcecode:: sh\n\n   sphinx-build -nW -b dirhtml docs/ html/\n\n\n.. _Sphinx: http://www.sphinx-doc.org/\n.. _unittest documentation: https://docs.python.org/3/library/unittest.html#command-line-interface\n"
  },
  {
    "path": "docs/training.rst",
    "content": "========\nTraining\n========\n\nChatterBot includes tools that help simplify the process of training a chat bot instance.\nChatterBot's training process involves loading example dialog into the chat bot's database.\nThis either creates or builds upon the graph data structure that represents the sets of\nknown statements and responses. When a chat bot trainer is provided with a data set,\nit creates the necessary entries in the chat bot's knowledge graph so that the statement\ninputs and responses are correctly represented.\n\n.. image:: _static/training-graph.svg\n   :alt: ChatterBot training statement graph\n\nSeveral training classes come built-in with ChatterBot. These utilities range from allowing\nyou to update the chat bot's database knowledge graph based on a list of statements\nrepresenting a conversation, to tools that allow you to train your bot based on a corpus of\npre-loaded training data.\n\nYou can also create your own training class. This is recommended if you wish to train your bot\nwith data you have stored in a format that is not already supported by one of the pre-built\nclasses listed below.\n\nTraining classes\n================\n\nChatterBot comes with training classes built in, or you can create your own\nif needed.\n\nTo use a training class you call `train()` on an instance that\nhas been initialized with your chat bot as shown in the following examples.\n\nTraining via list data\n----------------------\n\n.. autoclass:: chatterbot.trainers.ListTrainer\n   :members: train\n\nFor the training process, you will need to pass in a list of statements where the order of each statement is based\non its placement in a given conversation.\n\nFor example, if you were to run bot of the following training calls, then the resulting chatterbot would respond to\nboth statements of \"Hi there!\" and \"Greetings!\" by saying \"Hello\".\n\n.. code-block:: python\n   :caption: chatbot.py\n\n    chatbot = ChatBot('Training Example')\n\n.. code-block:: python\n   :caption: train.py\n\n   from chatbot import chatbot\n   from chatterbot.trainers import ListTrainer\n   \n   trainer = ListTrainer(chatbot)\n\n   trainer.train([\n       \"Hi there!\",\n       \"Hello\",\n   ])\n\n   trainer.train([\n       \"Greetings!\",\n       \"Hello\",\n   ])\n\nYou can also provide longer lists of training conversations.\nThis will establish each item in the list as a possible response to it's predecessor in the list.\n\n.. code-block:: python\n   :caption: train.py\n\n   trainer.train([\n       \"How are you?\",\n       \"I am good.\",\n       \"That is good to hear.\",\n       \"Thank you\",\n       \"You are welcome.\",\n   ])\n\n\nTraining with corpus data\n-------------------------\n\n.. autoclass:: chatterbot.trainers.ChatterBotCorpusTrainer\n   :members: train\n\nChatterBot comes with a corpus data and utility module that makes it easy to\nquickly train your bot to communicate. To do so, simply specify the corpus\ndata modules you want to use.\n\n.. code-block:: python\n   :caption: chatbot.py\n\n   chatbot = ChatBot('Training Example')\n\n.. code-block:: python\n   :caption: train.py\n\n   from chatbot import chatbot\n   from chatterbot.trainers import ChatterBotCorpusTrainer\n\n   trainer = ChatterBotCorpusTrainer(chatbot)\n\n   trainer.train(\n       \"chatterbot.corpus.english\"\n   )\n\nSpecifying corpus scope\n+++++++++++++++++++++++\n\nIt is also possible to import individual subsets of ChatterBot's corpus at once.\nFor example, if you only wish to train based on the english greetings and\nconversations corpora then you would simply specify them.\n\n.. code-block:: python\n   :caption: train.py\n\n   trainer.train(\n       \"chatterbot.corpus.english.greetings\",\n       \"chatterbot.corpus.english.conversations\"\n   )\n\nYou can also specify file paths to corpus files or directories of corpus files when calling the ``train`` method.\n\n.. code-block:: python\n   :caption: train.py\n\n   trainer.train(\n       \"./data/greetings_corpus/custom.corpus.yml\",\n       \"./data/my_corpus/\"\n   )\n\n\nTraining with CSV or TSV formatted data\n---------------------------------------\n\n.. autoclass:: chatterbot.trainers.CsvFileTrainer\n   :members: train\n\n   .. autoattribute:: chatterbot.trainers.CsvFileTrainer.DEFAULT_STATEMENT_TO_HEADER_MAPPING\n\n\nExample CSV data format:\n\n.. literalinclude:: ../tests/training/test_data/csv_corpus/1.csv\n   :caption: /data/training_data_1.csv\n\n.. code-block:: python\n   :caption: train.py\n\n   from chatterbot.trainers import CsvFileTrainer\n\n   trainer = CsvFileTrainer(\n       chatbot,\n       field_map={\n            'created_at': 0,\n            'persona': 1,\n            'text': 2,\n            'conversation': 3\n       }\n   )\n\n   trainer.train('./data/training_data_1.csv')\n\n\nTraining with JSON formatted data\n---------------------------------\n\n.. autoclass:: chatterbot.trainers.JsonFileTrainer\n   :members: train\n\n   .. autoattribute:: chatterbot.trainers.JsonFileTrainer.DEFAULT_STATEMENT_TO_KEY_MAPPING\n\n\nExample JSON data format:\n\n.. literalinclude:: ../tests/training/test_data/json_corpus/1.json\n   :caption: /data/training_data_1.json\n   :language: json\n\n.. code-block:: python\n   :caption: train.py\n\n   from chatterbot.trainers import JsonFileTrainer\n\n   trainer = JsonFileTrainer(\n       chatbot,\n       field_map={\n          'persona': 'persona',\n          'text': 'text',\n          'conversation': 'conversation',\n          'in_response_to': 'in_response_to',\n       }\n   )\n\n   trainer.train('./data/training_data_1.json')\n\n\nTraining with the Ubuntu dialog corpus\n--------------------------------------\n\n.. warning::\n\n   The Ubuntu dialog corpus is a massive data set. Developers will currently\n   experience significantly decreased performance in the form of delayed\n   training and response times from the chat bot when using this corpus.\n\n.. autoclass:: chatterbot.trainers.UbuntuCorpusTrainer\n   :members: train\n\nThis training class makes it possible to train your chat bot using the Ubuntu\ndialog corpus. Because of the file size of the Ubuntu dialog corpus, the download\nand training process may take a considerable amount of time.\n\nThis training class will handle the process of downloading the compressed corpus\nfile and extracting it. If the file has already been downloaded, it will not be\ndownloaded again. If the file is already extracted, it will not be extracted again.\n\n\nCreating a new training class\n=============================\n\nYou can create a new trainer to train your chat bot from your own\ndata files. You may choose to do this if you want to train your\nchat bot from a data source in a format that is not directly supported\nby ChatterBot.\n\nYour custom trainer should inherit `chatterbot.trainers.Trainer` class.\nYour trainer will need to have a method named `train`, that can take any\nparameters you choose.\n\nTake a look at the existing `trainer classes on GitHub`_ for examples.\n\n.. _`trainer classes on GitHub`: https://github.com/gunthercox/ChatterBot/blob/master/chatterbot/trainers.py\n"
  },
  {
    "path": "docs/tutorial.rst",
    "content": "===================\nChatterBot Tutorial\n===================\n\nThis tutorial will guide you through the process of creating a simple command-line chat bot using ChatterBot.\n\nGetting help\n============\n\nIf you’re having trouble with this tutorial, you can post a message on Gitter_\nto chat with other ChatterBot users who might be able to help.\n\nYou can also `ask questions`_ on `Stack Overflow`_ under the ``chatterbot`` tag.\n\nIf you believe that you have encountered an error in ChatterBot, please open a\nticket on GitHub: https://github.com/gunthercox/ChatterBot/issues/new\n\nInstalling ChatterBot\n=====================\n\nYou can install ChatterBot on your system using Python's pip command.\n\n.. code-block:: bash\n\n   pip install chatterbot\n\nSee :ref:`Installation` for alternative installation options.\n\nCreating your first chat bot\n============================\n\nCreate a new file named ``chatbot.py``.\nThen open ``chatbot.py`` in your editor of choice.\n\nBefore we do anything else, ChatterBot needs to be imported.\nThe import for ChatterBot should look like the following line.\n\n.. code-block:: python\n   :caption: chatbot.py\n\n   from chatterbot import ChatBot\n\nCreate a new instance of the ``ChatBot`` class.\n\n.. code-block:: python\n   :caption: chatbot.py\n\n   bot = ChatBot('Norman')\n\nThis line of code has created a new chat bot named `Norman`.\nThere is a few more parameters that we will want to specify\nbefore we run our program for the first time.\n\nSetting the storage adapter\n---------------------------\n\nChatterBot comes with built in adapter classes that allow it to connect\nto different types of databases. In this tutorial, we will be using the\n``SQLStorageAdapter`` which allows the chat bot to connect to SQL databases.\nBy default, this adapter will create a `SQLite`_ database.\n\nThe ``database`` parameter is used to specify the path to the database\nthat the chat bot will use. For this example we will call the database\n`sqlite:///database.sqlite3`. this file will be created automatically if it doesn't\nalready exist.\n\n.. code-block:: python\n   :caption: chatbot.py\n\n   bot = ChatBot(\n       'Norman',\n       storage_adapter='chatterbot.storage.SQLStorageAdapter',\n       database_uri='sqlite:///database.sqlite3'\n   )\n\n.. note::\n\n   The SQLStorageAdapter is ChatterBot's default adapter.\n   If you do not specify an adapter in your constructor,\n   the SQLStorageAdapter adapter will be used automatically.\n\nSpecifying logic adapters\n-------------------------\n\nThe `logic_adapters` parameter is a list of logic adapters.\nIn ChatterBot, a logic adapter is a class that takes an input statement\nand returns a response to that statement.\n\nYou can choose to use as many logic adapters as you would like.\nIn this example we will use two logic adapters. The TimeLogicAdapter returns\nthe current time when the input statement asks for it.\nThe MathematicalEvaluation adapter solves math problems that use basic\noperations.\n\n.. code-block:: python\n   :caption: chatbot.py\n\n   bot = ChatBot(\n       'Norman',\n       storage_adapter='chatterbot.storage.SQLStorageAdapter',\n       logic_adapters=[\n           'chatterbot.logic.MathematicalEvaluation',\n           'chatterbot.logic.TimeLogicAdapter'\n       ],\n       database_uri='sqlite:///database.sqlite3'\n   )\n\nGetting a response from your chat bot\n-------------------------------------\n\nNext, you will want to create a while loop for your chat bot to run in.\nBy breaking out of the loop when specific exceptions are triggered,\nwe can exit the loop and stop the program when a user enters `ctrl+c`.\n\n.. code-block:: python\n   :caption: chatbot.py\n\n   while True:\n       try:\n           bot_input = bot.get_response(input())\n           print(bot_input)\n\n       except(KeyboardInterrupt, EOFError, SystemExit):\n           break\n\nTraining your chat bot\n----------------------\n\nAt this point your chat bot, Norman will learn to communicate as you talk to him.\nYou can speed up this process by training him with examples of existing conversations.\n\n.. code-block:: python\n   :caption: chatbot.py\n\n   from chatterbot.trainers import ListTrainer\n\n   trainer = ListTrainer(bot)\n\n   trainer.train([\n       'How are you?',\n       'I am good.',\n       'That is good to hear.',\n       'Thank you',\n       'You are welcome.',\n   ])\n\nYou can run the training process multiple times to reinforce preferred responses\nto particular input statements. You can also run the train command on a number\nof different example dialogs to increase the breadth of inputs that your chat\nbot can respond to.\n\nConclusion and Next Steps\n--------------------------\n\nThis concludes this ChatterBot tutorial. Please see other sections of the\ndocumentation for more details and examples.\n\nFor more examples of using ChatterBot, see the :doc:`./examples` section. Additional tutorials and other articles that cover topic beyond the scope of this one can be found at the ChatterBot homepage: https://chatterbot.us/\n\n\n.. _Gitter: https://gitter.im/chatterbot/Lobby\n.. _SQLite: https://www.sqlite.org/\n.. _`Stack Overflow`: https://stackoverflow.com/questions/tagged/chatterbot\n.. _`ask questions`: https://stackoverflow.com/questions/ask\n"
  },
  {
    "path": "docs/upgrading.rst",
    "content": "===========================\nUpgrading to Newer Releases\n===========================\n\nLike any software, changes will be made to ChatterBot over time.\nMost of these changes are improvements. Frequently, you don't have\nto change anything in your code to benefit from a new release.\n\nOccasionally there are changes that will require modifications in\nyour code or there will be changes that make it possible for you\nto improve your code by taking advantage of new features.\n\nTo view a record of ChatterBot's history of changes, visit the\nreleases tab on ChatterBot's GitHub page.\n\n- https://github.com/gunthercox/ChatterBot/releases\n\nUse the pip command to upgrade your existing ChatterBot\ninstallation by providing the --upgrade parameter:\n\n.. code-block:: bash\n\n   pip install chatterbot --upgrade\n\nAlso see :ref:`Versioning` for information about ChatterBot's versioning policy.\n"
  },
  {
    "path": "docs/utils.rst",
    "content": "===============\nUtility Methods\n===============\n\nChatterBot has a utility module that contains\na collection of miscellaneous but useful functions.\n\n\nModule imports\n--------------\n\n.. autofunction:: chatterbot.utils.import_module\n\n\nClass initialization\n--------------------\n\n.. autofunction:: chatterbot.utils.initialize_class\n\n\nChatBot response time\n---------------------\n\n.. autofunction:: chatterbot.utils.get_response_time\n\n\nParsing datetime information\n----------------------------\n\n.. autofunction:: chatterbot.parsing.datetime_parsing\n"
  },
  {
    "path": "examples/__init__.py",
    "content": ""
  },
  {
    "path": "examples/basic_example.py",
    "content": "from chatterbot import ChatBot\nfrom chatterbot.trainers import ListTrainer\n\n# Create a new chat bot named Charlie\nchatbot = ChatBot('Charlie')\n\ntrainer = ListTrainer(chatbot)\n\ntrainer.train([\n    \"Hi, can I help you?\",\n    \"Sure, I'd like to book a flight to Iceland.\",\n    \"Your flight has been booked.\"\n])\n\n# Get a response to the input text 'I would like to book a flight.'\nresponse = chatbot.get_response('I would like to book a flight.')\n\nprint(response)\n"
  },
  {
    "path": "examples/convert_units.py",
    "content": "from chatterbot import ChatBot\n\n\nbot = ChatBot(\n    'Unit Converter',\n    logic_adapters=[\n        'chatterbot.logic.UnitConversion',\n    ]\n)\n\nquestions = [\n    'How many meters are in a kilometer?',\n    'How many meters are in one inch?',\n    '0 celsius to fahrenheit',\n    'one hour is how many minutes ?'\n]\n\n# Prints the convertion given the specific question\nfor question in questions:\n    response = bot.get_response(question)\n    print(question + ' -  Response: ' + response.text)\n"
  },
  {
    "path": "examples/default_response_example.py",
    "content": "from chatterbot import ChatBot\nfrom chatterbot.trainers import ListTrainer\n\n\n# Create a new instance of a ChatBot\nbot = ChatBot(\n    'Example Bot',\n    storage_adapter='chatterbot.storage.SQLStorageAdapter',\n    logic_adapters=[\n        {\n            'import_path': 'chatterbot.logic.BestMatch',\n            'default_response': 'I am sorry, but I do not understand.',\n            'maximum_similarity_threshold': 0.90\n        }\n    ]\n)\n\ntrainer = ListTrainer(bot)\n\n# Train the chat bot with a few responses\ntrainer.train([\n    'How can I help you?',\n    'I want to create a chat bot',\n    'Have you read the documentation?',\n    'No, I have not',\n    'This should help get you started: https://docs.chatterbot.us/quickstart/'\n])\n\n# Get a response for some unexpected input\nresponse = bot.get_response('How do I make an omelette?')\nprint(response)\n"
  },
  {
    "path": "examples/django_example/README.rst",
    "content": "=========================\nChatterBot Django Example\n=========================\n\nThis is an example Django app that shows how to create a simple chat bot web\napp using Django_ and ChatterBot_.\n\nQuick Start\n-----------\n\nTo run this example you will need to have Django and ChatterBot installed. The `requirements.txt` file contains the recommended versions of these packages for this example project.\n\n```bash\npip install -r requirements.txt\n```\n\nRun the Django migrations to populate ChatterBot database tables:\n\n```bash\npython manage.py migrate\n```\n\nStart the Django app by running the following:\n\n```bash\npython manage.py runserver 0.0.0.0:8000\n```\n\nDocumentation\n-------------\n\nFurther documentation on getting set up with Django and ChatterBot can be\nfound in the `ChatterBot documentation`_.\n\n.. _Django: https://www.djangoproject.com\n.. _ChatterBot: https://github.com/gunthercox/ChatterBot\n.. _ChatterBot documentation: https://docs.chatterbot.us/django/\n"
  },
  {
    "path": "examples/django_example/django_example/__init__.py",
    "content": ""
  },
  {
    "path": "examples/django_example/django_example/asgi.py",
    "content": "\"\"\"\nASGI config for django_example project.\n\nIt exposes the ASGI callable as a module-level variable named ``application``.\n\nFor more information on this file, see\nhttps://docs.djangoproject.com/en/4.2/howto/deployment/asgi/\n\"\"\"\n\nimport os\n\nfrom django.core.asgi import get_asgi_application\n\nos.environ.setdefault('DJANGO_SETTINGS_MODULE', 'django_example.settings')\n\napplication = get_asgi_application()\n"
  },
  {
    "path": "examples/django_example/django_example/management/__init__.py",
    "content": ""
  },
  {
    "path": "examples/django_example/django_example/management/commands/__init__.py",
    "content": ""
  },
  {
    "path": "examples/django_example/django_example/management/commands/train.py",
    "content": "\"\"\"\nThis is an example of a custom Django management command that\ntrains a ChatterBot instance with specified data.\n\nFor more information on how to create custom management commands,\nsee the Django documentation:\nhttps://docs.djangoproject.com/en/4.2/howto/custom-management-commands/\n\nFor details on the available training options for ChatterBot see:\nhttp://docs.chatterbot.us/training/ \n\"\"\"\n\nfrom django.core.management.base import BaseCommand\nfrom django.conf import settings\n\nfrom chatterbot import ChatBot\nfrom chatterbot.trainers import ListTrainer\n\n\nclass Command(BaseCommand):\n    help = 'Train a ChatterBot instance with specified data.'\n\n    def handle(self, *args, **options):\n        chatbot = ChatBot(**settings.CHATTERBOT)\n\n        trainer = ListTrainer(chatbot)\n\n        trainer.train([\n            'Hello, how are you?',\n            'I am good.',\n            'That is good to hear.',\n            'I am glad to hear that.',\n            'Thank you.',\n            'You are welcome.',\n        ])\n\n        self.stdout.write(\n            self.style.SUCCESS('Training completed successfully')\n        )\n"
  },
  {
    "path": "examples/django_example/django_example/settings.py",
    "content": "\"\"\"\nDjango settings for django_example project.\n\nGenerated by 'django-admin startproject' using Django 4.2.19.\n\nFor more information on this file, see\nhttps://docs.djangoproject.com/en/4.2/topics/settings/\n\nFor the full list of settings and their values, see\nhttps://docs.djangoproject.com/en/4.2/ref/settings/\n\"\"\"\n\nfrom pathlib import Path\n\n# Build paths inside the project like this: BASE_DIR / 'subdir'.\nBASE_DIR = Path(__file__).resolve().parent.parent\n\n\n# Quick-start development settings - unsuitable for production\n# See https://docs.djangoproject.com/en/4.2/howto/deployment/checklist/\n\n# SECURITY WARNING: keep the secret key used in production secret!\nSECRET_KEY = 'django-insecure-demo-key-123'\n\n# SECURITY WARNING: don't run with debug turned on in production!\nDEBUG = True\n\nALLOWED_HOSTS = []\n\n\n# Application definition\n\nINSTALLED_APPS = [\n    'django.contrib.admin',\n    'django.contrib.auth',\n    'django.contrib.contenttypes',\n    'django.contrib.sessions',\n    'django.contrib.messages',\n    'django.contrib.staticfiles',\n\n    'chatterbot.ext.django_chatterbot',\n\n    # Our example app:\n    'django_example',\n]\n\n# ChatterBot settings\n\nCHATTERBOT = {\n    'name': 'Django ChatterBot Example',\n    'django_app_name': 'django_chatterbot'\n}\n\nMIDDLEWARE = [\n    'django.middleware.security.SecurityMiddleware',\n    'django.contrib.sessions.middleware.SessionMiddleware',\n    'django.middleware.common.CommonMiddleware',\n    'django.middleware.csrf.CsrfViewMiddleware',\n    'django.contrib.auth.middleware.AuthenticationMiddleware',\n    'django.contrib.messages.middleware.MessageMiddleware',\n    'django.middleware.clickjacking.XFrameOptionsMiddleware',\n]\n\nROOT_URLCONF = 'django_example.urls'\n\nTEMPLATES = [\n    {\n        'BACKEND': 'django.template.backends.django.DjangoTemplates',\n        'DIRS': [BASE_DIR / 'django_example' / 'templates'],\n        'APP_DIRS': True,\n        'OPTIONS': {\n            'context_processors': [\n                'django.template.context_processors.debug',\n                'django.template.context_processors.request',\n                'django.contrib.auth.context_processors.auth',\n                'django.contrib.messages.context_processors.messages',\n            ],\n        },\n    },\n]\n\nWSGI_APPLICATION = 'django_example.wsgi.application'\n\n\n# Database\n# https://docs.djangoproject.com/en/4.2/ref/settings/#databases\n\nDATABASES = {\n    'default': {\n        'ENGINE': 'django.db.backends.sqlite3',\n        'NAME': BASE_DIR / 'db.sqlite3',\n    }\n}\n\n\n# Password validation\n# https://docs.djangoproject.com/en/4.2/ref/settings/#auth-password-validators\n\nAUTH_PASSWORD_VALIDATORS = []\n\n\n# Internationalization\n# https://docs.djangoproject.com/en/4.2/topics/i18n/\n\nLANGUAGE_CODE = 'en-us'\n\nTIME_ZONE = 'UTC'\n\nUSE_I18N = True\n\nUSE_TZ = True\n\n\n# Static files (CSS, JavaScript, Images)\n# https://docs.djangoproject.com/en/4.2/howto/static-files/\n\nSTATICFILES_DIRS = [\n    BASE_DIR / 'django_example' / 'static'\n]\n\nSTATIC_URL = 'static/'\n\n# Default primary key field type\n# https://docs.djangoproject.com/en/4.2/ref/settings/#default-auto-field\n\nDEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'\n"
  },
  {
    "path": "examples/django_example/django_example/static/css/bootstrap.css",
    "content": "/*!\n * Bootstrap v4.4.1 (https://getbootstrap.com/)\n * Copyright 2011-2019 The Bootstrap Authors\n * Copyright 2011-2019 Twitter, Inc.\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)\n */:root{--blue:#007bff;--indigo:#6610f2;--purple:#6f42c1;--pink:#e83e8c;--red:#dc3545;--orange:#fd7e14;--yellow:#ffc107;--green:#28a745;--teal:#20c997;--cyan:#17a2b8;--white:#fff;--gray:#6c757d;--gray-dark:#343a40;--primary:#007bff;--secondary:#6c757d;--success:#28a745;--info:#17a2b8;--warning:#ffc107;--danger:#dc3545;--light:#f8f9fa;--dark:#343a40;--breakpoint-xs:0;--breakpoint-sm:576px;--breakpoint-md:768px;--breakpoint-lg:992px;--breakpoint-xl:1200px;--font-family-sans-serif:-apple-system,BlinkMacSystemFont,\"Segoe UI\",Roboto,\"Helvetica Neue\",Arial,\"Noto Sans\",sans-serif,\"Apple Color Emoji\",\"Segoe UI Emoji\",\"Segoe UI Symbol\",\"Noto Color Emoji\";--font-family-monospace:SFMono-Regular,Menlo,Monaco,Consolas,\"Liberation Mono\",\"Courier New\",monospace}*,::after,::before{box-sizing:border-box}html{font-family:sans-serif;line-height:1.15;-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:transparent}article,aside,figcaption,figure,footer,header,hgroup,main,nav,section{display:block}body{margin:0;font-family:-apple-system,BlinkMacSystemFont,\"Segoe UI\",Roboto,\"Helvetica Neue\",Arial,\"Noto Sans\",sans-serif,\"Apple Color Emoji\",\"Segoe UI Emoji\",\"Segoe UI Symbol\",\"Noto Color Emoji\";font-size:1rem;font-weight:400;line-height:1.5;color:#212529;text-align:left;background-color:#fff}[tabindex=\"-1\"]:focus:not(:focus-visible){outline:0!important}hr{box-sizing:content-box;height:0;overflow:visible}h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:.5rem}p{margin-top:0;margin-bottom:1rem}abbr[data-original-title],abbr[title]{text-decoration:underline;-webkit-text-decoration:underline dotted;text-decoration:underline dotted;cursor:help;border-bottom:0;-webkit-text-decoration-skip-ink:none;text-decoration-skip-ink:none}address{margin-bottom:1rem;font-style:normal;line-height:inherit}dl,ol,ul{margin-top:0;margin-bottom:1rem}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}dt{font-weight:700}dd{margin-bottom:.5rem;margin-left:0}blockquote{margin:0 0 1rem}b,strong{font-weight:bolder}small{font-size:80%}sub,sup{position:relative;font-size:75%;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}a{color:#007bff;text-decoration:none;background-color:transparent}a:hover{color:#0056b3;text-decoration:underline}a:not([href]){color:inherit;text-decoration:none}a:not([href]):hover{color:inherit;text-decoration:none}code,kbd,pre,samp{font-family:SFMono-Regular,Menlo,Monaco,Consolas,\"Liberation Mono\",\"Courier New\",monospace;font-size:1em}pre{margin-top:0;margin-bottom:1rem;overflow:auto}figure{margin:0 0 1rem}img{vertical-align:middle;border-style:none}svg{overflow:hidden;vertical-align:middle}table{border-collapse:collapse}caption{padding-top:.75rem;padding-bottom:.75rem;color:#6c757d;text-align:left;caption-side:bottom}th{text-align:inherit}label{display:inline-block;margin-bottom:.5rem}button{border-radius:0}button:focus{outline:1px dotted;outline:5px auto -webkit-focus-ring-color}button,input,optgroup,select,textarea{margin:0;font-family:inherit;font-size:inherit;line-height:inherit}button,input{overflow:visible}button,select{text-transform:none}select{word-wrap:normal}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]:not(:disabled),[type=reset]:not(:disabled),[type=submit]:not(:disabled),button:not(:disabled){cursor:pointer}[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner,button::-moz-focus-inner{padding:0;border-style:none}input[type=checkbox],input[type=radio]{box-sizing:border-box;padding:0}input[type=date],input[type=datetime-local],input[type=month],input[type=time]{-webkit-appearance:listbox}textarea{overflow:auto;resize:vertical}fieldset{min-width:0;padding:0;margin:0;border:0}legend{display:block;width:100%;max-width:100%;padding:0;margin-bottom:.5rem;font-size:1.5rem;line-height:inherit;color:inherit;white-space:normal}progress{vertical-align:baseline}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{outline-offset:-2px;-webkit-appearance:none}[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}output{display:inline-block}summary{display:list-item;cursor:pointer}template{display:none}[hidden]{display:none!important}.h1,.h2,.h3,.h4,.h5,.h6,h1,h2,h3,h4,h5,h6{margin-bottom:.5rem;font-weight:500;line-height:1.2}.h1,h1{font-size:2.5rem}.h2,h2{font-size:2rem}.h3,h3{font-size:1.75rem}.h4,h4{font-size:1.5rem}.h5,h5{font-size:1.25rem}.h6,h6{font-size:1rem}.lead{font-size:1.25rem;font-weight:300}.display-1{font-size:6rem;font-weight:300;line-height:1.2}.display-2{font-size:5.5rem;font-weight:300;line-height:1.2}.display-3{font-size:4.5rem;font-weight:300;line-height:1.2}.display-4{font-size:3.5rem;font-weight:300;line-height:1.2}hr{margin-top:1rem;margin-bottom:1rem;border:0;border-top:1px solid rgba(0,0,0,.1)}.small,small{font-size:80%;font-weight:400}.mark,mark{padding:.2em;background-color:#fcf8e3}.list-unstyled{padding-left:0;list-style:none}.list-inline{padding-left:0;list-style:none}.list-inline-item{display:inline-block}.list-inline-item:not(:last-child){margin-right:.5rem}.initialism{font-size:90%;text-transform:uppercase}.blockquote{margin-bottom:1rem;font-size:1.25rem}.blockquote-footer{display:block;font-size:80%;color:#6c757d}.blockquote-footer::before{content:\"\\2014\\00A0\"}.img-fluid{max-width:100%;height:auto}.img-thumbnail{padding:.25rem;background-color:#fff;border:1px solid #dee2e6;border-radius:.25rem;max-width:100%;height:auto}.figure{display:inline-block}.figure-img{margin-bottom:.5rem;line-height:1}.figure-caption{font-size:90%;color:#6c757d}code{font-size:87.5%;color:#e83e8c;word-wrap:break-word}a>code{color:inherit}kbd{padding:.2rem .4rem;font-size:87.5%;color:#fff;background-color:#212529;border-radius:.2rem}kbd kbd{padding:0;font-size:100%;font-weight:700}pre{display:block;font-size:87.5%;color:#212529}pre code{font-size:inherit;color:inherit;word-break:normal}.pre-scrollable{max-height:340px;overflow-y:scroll}.container{width:100%;padding-right:15px;padding-left:15px;margin-right:auto;margin-left:auto}@media (min-width:576px){.container{max-width:540px}}@media (min-width:768px){.container{max-width:720px}}@media (min-width:992px){.container{max-width:960px}}@media (min-width:1200px){.container{max-width:1140px}}.container-fluid,.container-lg,.container-md,.container-sm,.container-xl{width:100%;padding-right:15px;padding-left:15px;margin-right:auto;margin-left:auto}@media (min-width:576px){.container,.container-sm{max-width:540px}}@media (min-width:768px){.container,.container-md,.container-sm{max-width:720px}}@media (min-width:992px){.container,.container-lg,.container-md,.container-sm{max-width:960px}}@media (min-width:1200px){.container,.container-lg,.container-md,.container-sm,.container-xl{max-width:1140px}}.row{display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;margin-right:-15px;margin-left:-15px}.no-gutters{margin-right:0;margin-left:0}.no-gutters>.col,.no-gutters>[class*=col-]{padding-right:0;padding-left:0}.col,.col-1,.col-10,.col-11,.col-12,.col-2,.col-3,.col-4,.col-5,.col-6,.col-7,.col-8,.col-9,.col-auto,.col-lg,.col-lg-1,.col-lg-10,.col-lg-11,.col-lg-12,.col-lg-2,.col-lg-3,.col-lg-4,.col-lg-5,.col-lg-6,.col-lg-7,.col-lg-8,.col-lg-9,.col-lg-auto,.col-md,.col-md-1,.col-md-10,.col-md-11,.col-md-12,.col-md-2,.col-md-3,.col-md-4,.col-md-5,.col-md-6,.col-md-7,.col-md-8,.col-md-9,.col-md-auto,.col-sm,.col-sm-1,.col-sm-10,.col-sm-11,.col-sm-12,.col-sm-2,.col-sm-3,.col-sm-4,.col-sm-5,.col-sm-6,.col-sm-7,.col-sm-8,.col-sm-9,.col-sm-auto,.col-xl,.col-xl-1,.col-xl-10,.col-xl-11,.col-xl-12,.col-xl-2,.col-xl-3,.col-xl-4,.col-xl-5,.col-xl-6,.col-xl-7,.col-xl-8,.col-xl-9,.col-xl-auto{position:relative;width:100%;padding-right:15px;padding-left:15px}.col{-ms-flex-preferred-size:0;flex-basis:0;-ms-flex-positive:1;flex-grow:1;max-width:100%}.row-cols-1>*{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.row-cols-2>*{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.row-cols-3>*{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.row-cols-4>*{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.row-cols-5>*{-ms-flex:0 0 20%;flex:0 0 20%;max-width:20%}.row-cols-6>*{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto;max-width:100%}.col-1{-ms-flex:0 0 8.333333%;flex:0 0 8.333333%;max-width:8.333333%}.col-2{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-3{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-4{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.col-5{-ms-flex:0 0 41.666667%;flex:0 0 41.666667%;max-width:41.666667%}.col-6{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-7{-ms-flex:0 0 58.333333%;flex:0 0 58.333333%;max-width:58.333333%}.col-8{-ms-flex:0 0 66.666667%;flex:0 0 66.666667%;max-width:66.666667%}.col-9{-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-10{-ms-flex:0 0 83.333333%;flex:0 0 83.333333%;max-width:83.333333%}.col-11{-ms-flex:0 0 91.666667%;flex:0 0 91.666667%;max-width:91.666667%}.col-12{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.order-first{-ms-flex-order:-1;order:-1}.order-last{-ms-flex-order:13;order:13}.order-0{-ms-flex-order:0;order:0}.order-1{-ms-flex-order:1;order:1}.order-2{-ms-flex-order:2;order:2}.order-3{-ms-flex-order:3;order:3}.order-4{-ms-flex-order:4;order:4}.order-5{-ms-flex-order:5;order:5}.order-6{-ms-flex-order:6;order:6}.order-7{-ms-flex-order:7;order:7}.order-8{-ms-flex-order:8;order:8}.order-9{-ms-flex-order:9;order:9}.order-10{-ms-flex-order:10;order:10}.order-11{-ms-flex-order:11;order:11}.order-12{-ms-flex-order:12;order:12}.offset-1{margin-left:8.333333%}.offset-2{margin-left:16.666667%}.offset-3{margin-left:25%}.offset-4{margin-left:33.333333%}.offset-5{margin-left:41.666667%}.offset-6{margin-left:50%}.offset-7{margin-left:58.333333%}.offset-8{margin-left:66.666667%}.offset-9{margin-left:75%}.offset-10{margin-left:83.333333%}.offset-11{margin-left:91.666667%}@media (min-width:576px){.col-sm{-ms-flex-preferred-size:0;flex-basis:0;-ms-flex-positive:1;flex-grow:1;max-width:100%}.row-cols-sm-1>*{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.row-cols-sm-2>*{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.row-cols-sm-3>*{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.row-cols-sm-4>*{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.row-cols-sm-5>*{-ms-flex:0 0 20%;flex:0 0 20%;max-width:20%}.row-cols-sm-6>*{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-sm-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto;max-width:100%}.col-sm-1{-ms-flex:0 0 8.333333%;flex:0 0 8.333333%;max-width:8.333333%}.col-sm-2{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-sm-3{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-sm-4{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.col-sm-5{-ms-flex:0 0 41.666667%;flex:0 0 41.666667%;max-width:41.666667%}.col-sm-6{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-sm-7{-ms-flex:0 0 58.333333%;flex:0 0 58.333333%;max-width:58.333333%}.col-sm-8{-ms-flex:0 0 66.666667%;flex:0 0 66.666667%;max-width:66.666667%}.col-sm-9{-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-sm-10{-ms-flex:0 0 83.333333%;flex:0 0 83.333333%;max-width:83.333333%}.col-sm-11{-ms-flex:0 0 91.666667%;flex:0 0 91.666667%;max-width:91.666667%}.col-sm-12{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.order-sm-first{-ms-flex-order:-1;order:-1}.order-sm-last{-ms-flex-order:13;order:13}.order-sm-0{-ms-flex-order:0;order:0}.order-sm-1{-ms-flex-order:1;order:1}.order-sm-2{-ms-flex-order:2;order:2}.order-sm-3{-ms-flex-order:3;order:3}.order-sm-4{-ms-flex-order:4;order:4}.order-sm-5{-ms-flex-order:5;order:5}.order-sm-6{-ms-flex-order:6;order:6}.order-sm-7{-ms-flex-order:7;order:7}.order-sm-8{-ms-flex-order:8;order:8}.order-sm-9{-ms-flex-order:9;order:9}.order-sm-10{-ms-flex-order:10;order:10}.order-sm-11{-ms-flex-order:11;order:11}.order-sm-12{-ms-flex-order:12;order:12}.offset-sm-0{margin-left:0}.offset-sm-1{margin-left:8.333333%}.offset-sm-2{margin-left:16.666667%}.offset-sm-3{margin-left:25%}.offset-sm-4{margin-left:33.333333%}.offset-sm-5{margin-left:41.666667%}.offset-sm-6{margin-left:50%}.offset-sm-7{margin-left:58.333333%}.offset-sm-8{margin-left:66.666667%}.offset-sm-9{margin-left:75%}.offset-sm-10{margin-left:83.333333%}.offset-sm-11{margin-left:91.666667%}}@media (min-width:768px){.col-md{-ms-flex-preferred-size:0;flex-basis:0;-ms-flex-positive:1;flex-grow:1;max-width:100%}.row-cols-md-1>*{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.row-cols-md-2>*{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.row-cols-md-3>*{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.row-cols-md-4>*{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.row-cols-md-5>*{-ms-flex:0 0 20%;flex:0 0 20%;max-width:20%}.row-cols-md-6>*{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-md-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto;max-width:100%}.col-md-1{-ms-flex:0 0 8.333333%;flex:0 0 8.333333%;max-width:8.333333%}.col-md-2{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-md-3{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-md-4{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.col-md-5{-ms-flex:0 0 41.666667%;flex:0 0 41.666667%;max-width:41.666667%}.col-md-6{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-md-7{-ms-flex:0 0 58.333333%;flex:0 0 58.333333%;max-width:58.333333%}.col-md-8{-ms-flex:0 0 66.666667%;flex:0 0 66.666667%;max-width:66.666667%}.col-md-9{-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-md-10{-ms-flex:0 0 83.333333%;flex:0 0 83.333333%;max-width:83.333333%}.col-md-11{-ms-flex:0 0 91.666667%;flex:0 0 91.666667%;max-width:91.666667%}.col-md-12{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.order-md-first{-ms-flex-order:-1;order:-1}.order-md-last{-ms-flex-order:13;order:13}.order-md-0{-ms-flex-order:0;order:0}.order-md-1{-ms-flex-order:1;order:1}.order-md-2{-ms-flex-order:2;order:2}.order-md-3{-ms-flex-order:3;order:3}.order-md-4{-ms-flex-order:4;order:4}.order-md-5{-ms-flex-order:5;order:5}.order-md-6{-ms-flex-order:6;order:6}.order-md-7{-ms-flex-order:7;order:7}.order-md-8{-ms-flex-order:8;order:8}.order-md-9{-ms-flex-order:9;order:9}.order-md-10{-ms-flex-order:10;order:10}.order-md-11{-ms-flex-order:11;order:11}.order-md-12{-ms-flex-order:12;order:12}.offset-md-0{margin-left:0}.offset-md-1{margin-left:8.333333%}.offset-md-2{margin-left:16.666667%}.offset-md-3{margin-left:25%}.offset-md-4{margin-left:33.333333%}.offset-md-5{margin-left:41.666667%}.offset-md-6{margin-left:50%}.offset-md-7{margin-left:58.333333%}.offset-md-8{margin-left:66.666667%}.offset-md-9{margin-left:75%}.offset-md-10{margin-left:83.333333%}.offset-md-11{margin-left:91.666667%}}@media (min-width:992px){.col-lg{-ms-flex-preferred-size:0;flex-basis:0;-ms-flex-positive:1;flex-grow:1;max-width:100%}.row-cols-lg-1>*{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.row-cols-lg-2>*{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.row-cols-lg-3>*{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.row-cols-lg-4>*{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.row-cols-lg-5>*{-ms-flex:0 0 20%;flex:0 0 20%;max-width:20%}.row-cols-lg-6>*{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-lg-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto;max-width:100%}.col-lg-1{-ms-flex:0 0 8.333333%;flex:0 0 8.333333%;max-width:8.333333%}.col-lg-2{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-lg-3{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-lg-4{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.col-lg-5{-ms-flex:0 0 41.666667%;flex:0 0 41.666667%;max-width:41.666667%}.col-lg-6{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-lg-7{-ms-flex:0 0 58.333333%;flex:0 0 58.333333%;max-width:58.333333%}.col-lg-8{-ms-flex:0 0 66.666667%;flex:0 0 66.666667%;max-width:66.666667%}.col-lg-9{-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-lg-10{-ms-flex:0 0 83.333333%;flex:0 0 83.333333%;max-width:83.333333%}.col-lg-11{-ms-flex:0 0 91.666667%;flex:0 0 91.666667%;max-width:91.666667%}.col-lg-12{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.order-lg-first{-ms-flex-order:-1;order:-1}.order-lg-last{-ms-flex-order:13;order:13}.order-lg-0{-ms-flex-order:0;order:0}.order-lg-1{-ms-flex-order:1;order:1}.order-lg-2{-ms-flex-order:2;order:2}.order-lg-3{-ms-flex-order:3;order:3}.order-lg-4{-ms-flex-order:4;order:4}.order-lg-5{-ms-flex-order:5;order:5}.order-lg-6{-ms-flex-order:6;order:6}.order-lg-7{-ms-flex-order:7;order:7}.order-lg-8{-ms-flex-order:8;order:8}.order-lg-9{-ms-flex-order:9;order:9}.order-lg-10{-ms-flex-order:10;order:10}.order-lg-11{-ms-flex-order:11;order:11}.order-lg-12{-ms-flex-order:12;order:12}.offset-lg-0{margin-left:0}.offset-lg-1{margin-left:8.333333%}.offset-lg-2{margin-left:16.666667%}.offset-lg-3{margin-left:25%}.offset-lg-4{margin-left:33.333333%}.offset-lg-5{margin-left:41.666667%}.offset-lg-6{margin-left:50%}.offset-lg-7{margin-left:58.333333%}.offset-lg-8{margin-left:66.666667%}.offset-lg-9{margin-left:75%}.offset-lg-10{margin-left:83.333333%}.offset-lg-11{margin-left:91.666667%}}@media (min-width:1200px){.col-xl{-ms-flex-preferred-size:0;flex-basis:0;-ms-flex-positive:1;flex-grow:1;max-width:100%}.row-cols-xl-1>*{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.row-cols-xl-2>*{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.row-cols-xl-3>*{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.row-cols-xl-4>*{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.row-cols-xl-5>*{-ms-flex:0 0 20%;flex:0 0 20%;max-width:20%}.row-cols-xl-6>*{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-xl-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto;max-width:100%}.col-xl-1{-ms-flex:0 0 8.333333%;flex:0 0 8.333333%;max-width:8.333333%}.col-xl-2{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-xl-3{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-xl-4{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.col-xl-5{-ms-flex:0 0 41.666667%;flex:0 0 41.666667%;max-width:41.666667%}.col-xl-6{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-xl-7{-ms-flex:0 0 58.333333%;flex:0 0 58.333333%;max-width:58.333333%}.col-xl-8{-ms-flex:0 0 66.666667%;flex:0 0 66.666667%;max-width:66.666667%}.col-xl-9{-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-xl-10{-ms-flex:0 0 83.333333%;flex:0 0 83.333333%;max-width:83.333333%}.col-xl-11{-ms-flex:0 0 91.666667%;flex:0 0 91.666667%;max-width:91.666667%}.col-xl-12{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.order-xl-first{-ms-flex-order:-1;order:-1}.order-xl-last{-ms-flex-order:13;order:13}.order-xl-0{-ms-flex-order:0;order:0}.order-xl-1{-ms-flex-order:1;order:1}.order-xl-2{-ms-flex-order:2;order:2}.order-xl-3{-ms-flex-order:3;order:3}.order-xl-4{-ms-flex-order:4;order:4}.order-xl-5{-ms-flex-order:5;order:5}.order-xl-6{-ms-flex-order:6;order:6}.order-xl-7{-ms-flex-order:7;order:7}.order-xl-8{-ms-flex-order:8;order:8}.order-xl-9{-ms-flex-order:9;order:9}.order-xl-10{-ms-flex-order:10;order:10}.order-xl-11{-ms-flex-order:11;order:11}.order-xl-12{-ms-flex-order:12;order:12}.offset-xl-0{margin-left:0}.offset-xl-1{margin-left:8.333333%}.offset-xl-2{margin-left:16.666667%}.offset-xl-3{margin-left:25%}.offset-xl-4{margin-left:33.333333%}.offset-xl-5{margin-left:41.666667%}.offset-xl-6{margin-left:50%}.offset-xl-7{margin-left:58.333333%}.offset-xl-8{margin-left:66.666667%}.offset-xl-9{margin-left:75%}.offset-xl-10{margin-left:83.333333%}.offset-xl-11{margin-left:91.666667%}}.table{width:100%;margin-bottom:1rem;color:#212529}.table td,.table th{padding:.75rem;vertical-align:top;border-top:1px solid #dee2e6}.table thead th{vertical-align:bottom;border-bottom:2px solid #dee2e6}.table tbody+tbody{border-top:2px solid #dee2e6}.table-sm td,.table-sm th{padding:.3rem}.table-bordered{border:1px solid #dee2e6}.table-bordered td,.table-bordered th{border:1px solid #dee2e6}.table-bordered thead td,.table-bordered thead th{border-bottom-width:2px}.table-borderless tbody+tbody,.table-borderless td,.table-borderless th,.table-borderless thead th{border:0}.table-striped tbody tr:nth-of-type(odd){background-color:rgba(0,0,0,.05)}.table-hover tbody tr:hover{color:#212529;background-color:rgba(0,0,0,.075)}.table-primary,.table-primary>td,.table-primary>th{background-color:#b8daff}.table-primary tbody+tbody,.table-primary td,.table-primary th,.table-primary thead th{border-color:#7abaff}.table-hover .table-primary:hover{background-color:#9fcdff}.table-hover .table-primary:hover>td,.table-hover .table-primary:hover>th{background-color:#9fcdff}.table-secondary,.table-secondary>td,.table-secondary>th{background-color:#d6d8db}.table-secondary tbody+tbody,.table-secondary td,.table-secondary th,.table-secondary thead th{border-color:#b3b7bb}.table-hover .table-secondary:hover{background-color:#c8cbcf}.table-hover .table-secondary:hover>td,.table-hover .table-secondary:hover>th{background-color:#c8cbcf}.table-success,.table-success>td,.table-success>th{background-color:#c3e6cb}.table-success tbody+tbody,.table-success td,.table-success th,.table-success thead th{border-color:#8fd19e}.table-hover .table-success:hover{background-color:#b1dfbb}.table-hover .table-success:hover>td,.table-hover .table-success:hover>th{background-color:#b1dfbb}.table-info,.table-info>td,.table-info>th{background-color:#bee5eb}.table-info tbody+tbody,.table-info td,.table-info th,.table-info thead th{border-color:#86cfda}.table-hover .table-info:hover{background-color:#abdde5}.table-hover .table-info:hover>td,.table-hover .table-info:hover>th{background-color:#abdde5}.table-warning,.table-warning>td,.table-warning>th{background-color:#ffeeba}.table-warning tbody+tbody,.table-warning td,.table-warning th,.table-warning thead th{border-color:#ffdf7e}.table-hover .table-warning:hover{background-color:#ffe8a1}.table-hover .table-warning:hover>td,.table-hover .table-warning:hover>th{background-color:#ffe8a1}.table-danger,.table-danger>td,.table-danger>th{background-color:#f5c6cb}.table-danger tbody+tbody,.table-danger td,.table-danger th,.table-danger thead th{border-color:#ed969e}.table-hover .table-danger:hover{background-color:#f1b0b7}.table-hover .table-danger:hover>td,.table-hover .table-danger:hover>th{background-color:#f1b0b7}.table-light,.table-light>td,.table-light>th{background-color:#fdfdfe}.table-light tbody+tbody,.table-light td,.table-light th,.table-light thead th{border-color:#fbfcfc}.table-hover .table-light:hover{background-color:#ececf6}.table-hover .table-light:hover>td,.table-hover .table-light:hover>th{background-color:#ececf6}.table-dark,.table-dark>td,.table-dark>th{background-color:#c6c8ca}.table-dark tbody+tbody,.table-dark td,.table-dark th,.table-dark thead th{border-color:#95999c}.table-hover .table-dark:hover{background-color:#b9bbbe}.table-hover .table-dark:hover>td,.table-hover .table-dark:hover>th{background-color:#b9bbbe}.table-active,.table-active>td,.table-active>th{background-color:rgba(0,0,0,.075)}.table-hover .table-active:hover{background-color:rgba(0,0,0,.075)}.table-hover .table-active:hover>td,.table-hover .table-active:hover>th{background-color:rgba(0,0,0,.075)}.table .thead-dark th{color:#fff;background-color:#343a40;border-color:#454d55}.table .thead-light th{color:#495057;background-color:#e9ecef;border-color:#dee2e6}.table-dark{color:#fff;background-color:#343a40}.table-dark td,.table-dark th,.table-dark thead th{border-color:#454d55}.table-dark.table-bordered{border:0}.table-dark.table-striped tbody tr:nth-of-type(odd){background-color:rgba(255,255,255,.05)}.table-dark.table-hover tbody tr:hover{color:#fff;background-color:rgba(255,255,255,.075)}@media (max-width:575.98px){.table-responsive-sm{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch}.table-responsive-sm>.table-bordered{border:0}}@media (max-width:767.98px){.table-responsive-md{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch}.table-responsive-md>.table-bordered{border:0}}@media (max-width:991.98px){.table-responsive-lg{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch}.table-responsive-lg>.table-bordered{border:0}}@media (max-width:1199.98px){.table-responsive-xl{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch}.table-responsive-xl>.table-bordered{border:0}}.table-responsive{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch}.table-responsive>.table-bordered{border:0}.form-control{display:block;width:100%;height:calc(1.5em + .75rem + 2px);padding:.375rem .75rem;font-size:1rem;font-weight:400;line-height:1.5;color:#495057;background-color:#fff;background-clip:padding-box;border:1px solid #ced4da;border-radius:.25rem;transition:border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.form-control{transition:none}}.form-control::-ms-expand{background-color:transparent;border:0}.form-control:-moz-focusring{color:transparent;text-shadow:0 0 0 #495057}.form-control:focus{color:#495057;background-color:#fff;border-color:#80bdff;outline:0;box-shadow:0 0 0 .2rem rgba(0,123,255,.25)}.form-control::-webkit-input-placeholder{color:#6c757d;opacity:1}.form-control::-moz-placeholder{color:#6c757d;opacity:1}.form-control:-ms-input-placeholder{color:#6c757d;opacity:1}.form-control::-ms-input-placeholder{color:#6c757d;opacity:1}.form-control::placeholder{color:#6c757d;opacity:1}.form-control:disabled,.form-control[readonly]{background-color:#e9ecef;opacity:1}select.form-control:focus::-ms-value{color:#495057;background-color:#fff}.form-control-file,.form-control-range{display:block;width:100%}.col-form-label{padding-top:calc(.375rem + 1px);padding-bottom:calc(.375rem + 1px);margin-bottom:0;font-size:inherit;line-height:1.5}.col-form-label-lg{padding-top:calc(.5rem + 1px);padding-bottom:calc(.5rem + 1px);font-size:1.25rem;line-height:1.5}.col-form-label-sm{padding-top:calc(.25rem + 1px);padding-bottom:calc(.25rem + 1px);font-size:.875rem;line-height:1.5}.form-control-plaintext{display:block;width:100%;padding:.375rem 0;margin-bottom:0;font-size:1rem;line-height:1.5;color:#212529;background-color:transparent;border:solid transparent;border-width:1px 0}.form-control-plaintext.form-control-lg,.form-control-plaintext.form-control-sm{padding-right:0;padding-left:0}.form-control-sm{height:calc(1.5em + .5rem + 2px);padding:.25rem .5rem;font-size:.875rem;line-height:1.5;border-radius:.2rem}.form-control-lg{height:calc(1.5em + 1rem + 2px);padding:.5rem 1rem;font-size:1.25rem;line-height:1.5;border-radius:.3rem}select.form-control[multiple],select.form-control[size]{height:auto}textarea.form-control{height:auto}.form-group{margin-bottom:1rem}.form-text{display:block;margin-top:.25rem}.form-row{display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;margin-right:-5px;margin-left:-5px}.form-row>.col,.form-row>[class*=col-]{padding-right:5px;padding-left:5px}.form-check{position:relative;display:block;padding-left:1.25rem}.form-check-input{position:absolute;margin-top:.3rem;margin-left:-1.25rem}.form-check-input:disabled~.form-check-label,.form-check-input[disabled]~.form-check-label{color:#6c757d}.form-check-label{margin-bottom:0}.form-check-inline{display:-ms-inline-flexbox;display:inline-flex;-ms-flex-align:center;align-items:center;padding-left:0;margin-right:.75rem}.form-check-inline .form-check-input{position:static;margin-top:0;margin-right:.3125rem;margin-left:0}.valid-feedback{display:none;width:100%;margin-top:.25rem;font-size:80%;color:#28a745}.valid-tooltip{position:absolute;top:100%;z-index:5;display:none;max-width:100%;padding:.25rem .5rem;margin-top:.1rem;font-size:.875rem;line-height:1.5;color:#fff;background-color:rgba(40,167,69,.9);border-radius:.25rem}.is-valid~.valid-feedback,.is-valid~.valid-tooltip,.was-validated :valid~.valid-feedback,.was-validated :valid~.valid-tooltip{display:block}.form-control.is-valid,.was-validated .form-control:valid{border-color:#28a745;padding-right:calc(1.5em + .75rem);background-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='8' height='8' viewBox='0 0 8 8'%3e%3cpath fill='%2328a745' d='M2.3 6.73L.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e\");background-repeat:no-repeat;background-position:right calc(.375em + .1875rem) center;background-size:calc(.75em + .375rem) calc(.75em + .375rem)}.form-control.is-valid:focus,.was-validated .form-control:valid:focus{border-color:#28a745;box-shadow:0 0 0 .2rem rgba(40,167,69,.25)}.was-validated textarea.form-control:valid,textarea.form-control.is-valid{padding-right:calc(1.5em + .75rem);background-position:top calc(.375em + .1875rem) right calc(.375em + .1875rem)}.custom-select.is-valid,.was-validated .custom-select:valid{border-color:#28a745;padding-right:calc(.75em + 2.3125rem);background:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='4' height='5' viewBox='0 0 4 5'%3e%3cpath fill='%23343a40' d='M2 0L0 2h4zm0 5L0 3h4z'/%3e%3c/svg%3e\") no-repeat right .75rem center/8px 10px,url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='8' height='8' viewBox='0 0 8 8'%3e%3cpath fill='%2328a745' d='M2.3 6.73L.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e\") #fff no-repeat center right 1.75rem/calc(.75em + .375rem) calc(.75em + .375rem)}.custom-select.is-valid:focus,.was-validated .custom-select:valid:focus{border-color:#28a745;box-shadow:0 0 0 .2rem rgba(40,167,69,.25)}.form-check-input.is-valid~.form-check-label,.was-validated .form-check-input:valid~.form-check-label{color:#28a745}.form-check-input.is-valid~.valid-feedback,.form-check-input.is-valid~.valid-tooltip,.was-validated .form-check-input:valid~.valid-feedback,.was-validated .form-check-input:valid~.valid-tooltip{display:block}.custom-control-input.is-valid~.custom-control-label,.was-validated .custom-control-input:valid~.custom-control-label{color:#28a745}.custom-control-input.is-valid~.custom-control-label::before,.was-validated .custom-control-input:valid~.custom-control-label::before{border-color:#28a745}.custom-control-input.is-valid:checked~.custom-control-label::before,.was-validated .custom-control-input:valid:checked~.custom-control-label::before{border-color:#34ce57;background-color:#34ce57}.custom-control-input.is-valid:focus~.custom-control-label::before,.was-validated .custom-control-input:valid:focus~.custom-control-label::before{box-shadow:0 0 0 .2rem rgba(40,167,69,.25)}.custom-control-input.is-valid:focus:not(:checked)~.custom-control-label::before,.was-validated .custom-control-input:valid:focus:not(:checked)~.custom-control-label::before{border-color:#28a745}.custom-file-input.is-valid~.custom-file-label,.was-validated .custom-file-input:valid~.custom-file-label{border-color:#28a745}.custom-file-input.is-valid:focus~.custom-file-label,.was-validated .custom-file-input:valid:focus~.custom-file-label{border-color:#28a745;box-shadow:0 0 0 .2rem rgba(40,167,69,.25)}.invalid-feedback{display:none;width:100%;margin-top:.25rem;font-size:80%;color:#dc3545}.invalid-tooltip{position:absolute;top:100%;z-index:5;display:none;max-width:100%;padding:.25rem .5rem;margin-top:.1rem;font-size:.875rem;line-height:1.5;color:#fff;background-color:rgba(220,53,69,.9);border-radius:.25rem}.is-invalid~.invalid-feedback,.is-invalid~.invalid-tooltip,.was-validated :invalid~.invalid-feedback,.was-validated :invalid~.invalid-tooltip{display:block}.form-control.is-invalid,.was-validated .form-control:invalid{border-color:#dc3545;padding-right:calc(1.5em + .75rem);background-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' fill='none' stroke='%23dc3545' viewBox='0 0 12 12'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23dc3545' stroke='none'/%3e%3c/svg%3e\");background-repeat:no-repeat;background-position:right calc(.375em + .1875rem) center;background-size:calc(.75em + .375rem) calc(.75em + .375rem)}.form-control.is-invalid:focus,.was-validated .form-control:invalid:focus{border-color:#dc3545;box-shadow:0 0 0 .2rem rgba(220,53,69,.25)}.was-validated textarea.form-control:invalid,textarea.form-control.is-invalid{padding-right:calc(1.5em + .75rem);background-position:top calc(.375em + .1875rem) right calc(.375em + .1875rem)}.custom-select.is-invalid,.was-validated .custom-select:invalid{border-color:#dc3545;padding-right:calc(.75em + 2.3125rem);background:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='4' height='5' viewBox='0 0 4 5'%3e%3cpath fill='%23343a40' d='M2 0L0 2h4zm0 5L0 3h4z'/%3e%3c/svg%3e\") no-repeat right .75rem center/8px 10px,url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' fill='none' stroke='%23dc3545' viewBox='0 0 12 12'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23dc3545' stroke='none'/%3e%3c/svg%3e\") #fff no-repeat center right 1.75rem/calc(.75em + .375rem) calc(.75em + .375rem)}.custom-select.is-invalid:focus,.was-validated .custom-select:invalid:focus{border-color:#dc3545;box-shadow:0 0 0 .2rem rgba(220,53,69,.25)}.form-check-input.is-invalid~.form-check-label,.was-validated .form-check-input:invalid~.form-check-label{color:#dc3545}.form-check-input.is-invalid~.invalid-feedback,.form-check-input.is-invalid~.invalid-tooltip,.was-validated .form-check-input:invalid~.invalid-feedback,.was-validated .form-check-input:invalid~.invalid-tooltip{display:block}.custom-control-input.is-invalid~.custom-control-label,.was-validated .custom-control-input:invalid~.custom-control-label{color:#dc3545}.custom-control-input.is-invalid~.custom-control-label::before,.was-validated .custom-control-input:invalid~.custom-control-label::before{border-color:#dc3545}.custom-control-input.is-invalid:checked~.custom-control-label::before,.was-validated .custom-control-input:invalid:checked~.custom-control-label::before{border-color:#e4606d;background-color:#e4606d}.custom-control-input.is-invalid:focus~.custom-control-label::before,.was-validated .custom-control-input:invalid:focus~.custom-control-label::before{box-shadow:0 0 0 .2rem rgba(220,53,69,.25)}.custom-control-input.is-invalid:focus:not(:checked)~.custom-control-label::before,.was-validated .custom-control-input:invalid:focus:not(:checked)~.custom-control-label::before{border-color:#dc3545}.custom-file-input.is-invalid~.custom-file-label,.was-validated .custom-file-input:invalid~.custom-file-label{border-color:#dc3545}.custom-file-input.is-invalid:focus~.custom-file-label,.was-validated .custom-file-input:invalid:focus~.custom-file-label{border-color:#dc3545;box-shadow:0 0 0 .2rem rgba(220,53,69,.25)}.form-inline{display:-ms-flexbox;display:flex;-ms-flex-flow:row wrap;flex-flow:row wrap;-ms-flex-align:center;align-items:center}.form-inline .form-check{width:100%}@media (min-width:576px){.form-inline label{display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;-ms-flex-pack:center;justify-content:center;margin-bottom:0}.form-inline .form-group{display:-ms-flexbox;display:flex;-ms-flex:0 0 auto;flex:0 0 auto;-ms-flex-flow:row wrap;flex-flow:row wrap;-ms-flex-align:center;align-items:center;margin-bottom:0}.form-inline .form-control{display:inline-block;width:auto;vertical-align:middle}.form-inline .form-control-plaintext{display:inline-block}.form-inline .custom-select,.form-inline .input-group{width:auto}.form-inline .form-check{display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;-ms-flex-pack:center;justify-content:center;width:auto;padding-left:0}.form-inline .form-check-input{position:relative;-ms-flex-negative:0;flex-shrink:0;margin-top:0;margin-right:.25rem;margin-left:0}.form-inline .custom-control{-ms-flex-align:center;align-items:center;-ms-flex-pack:center;justify-content:center}.form-inline .custom-control-label{margin-bottom:0}}.btn{display:inline-block;font-weight:400;color:#212529;text-align:center;vertical-align:middle;cursor:pointer;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;background-color:transparent;border:1px solid transparent;padding:.375rem .75rem;font-size:1rem;line-height:1.5;border-radius:.25rem;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.btn{transition:none}}.btn:hover{color:#212529;text-decoration:none}.btn.focus,.btn:focus{outline:0;box-shadow:0 0 0 .2rem rgba(0,123,255,.25)}.btn.disabled,.btn:disabled{opacity:.65}a.btn.disabled,fieldset:disabled a.btn{pointer-events:none}.btn-primary{color:#fff;background-color:#007bff;border-color:#007bff}.btn-primary:hover{color:#fff;background-color:#0069d9;border-color:#0062cc}.btn-primary.focus,.btn-primary:focus{color:#fff;background-color:#0069d9;border-color:#0062cc;box-shadow:0 0 0 .2rem rgba(38,143,255,.5)}.btn-primary.disabled,.btn-primary:disabled{color:#fff;background-color:#007bff;border-color:#007bff}.btn-primary:not(:disabled):not(.disabled).active,.btn-primary:not(:disabled):not(.disabled):active,.show>.btn-primary.dropdown-toggle{color:#fff;background-color:#0062cc;border-color:#005cbf}.btn-primary:not(:disabled):not(.disabled).active:focus,.btn-primary:not(:disabled):not(.disabled):active:focus,.show>.btn-primary.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(38,143,255,.5)}.btn-secondary{color:#fff;background-color:#6c757d;border-color:#6c757d}.btn-secondary:hover{color:#fff;background-color:#5a6268;border-color:#545b62}.btn-secondary.focus,.btn-secondary:focus{color:#fff;background-color:#5a6268;border-color:#545b62;box-shadow:0 0 0 .2rem rgba(130,138,145,.5)}.btn-secondary.disabled,.btn-secondary:disabled{color:#fff;background-color:#6c757d;border-color:#6c757d}.btn-secondary:not(:disabled):not(.disabled).active,.btn-secondary:not(:disabled):not(.disabled):active,.show>.btn-secondary.dropdown-toggle{color:#fff;background-color:#545b62;border-color:#4e555b}.btn-secondary:not(:disabled):not(.disabled).active:focus,.btn-secondary:not(:disabled):not(.disabled):active:focus,.show>.btn-secondary.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(130,138,145,.5)}.btn-success{color:#fff;background-color:#28a745;border-color:#28a745}.btn-success:hover{color:#fff;background-color:#218838;border-color:#1e7e34}.btn-success.focus,.btn-success:focus{color:#fff;background-color:#218838;border-color:#1e7e34;box-shadow:0 0 0 .2rem rgba(72,180,97,.5)}.btn-success.disabled,.btn-success:disabled{color:#fff;background-color:#28a745;border-color:#28a745}.btn-success:not(:disabled):not(.disabled).active,.btn-success:not(:disabled):not(.disabled):active,.show>.btn-success.dropdown-toggle{color:#fff;background-color:#1e7e34;border-color:#1c7430}.btn-success:not(:disabled):not(.disabled).active:focus,.btn-success:not(:disabled):not(.disabled):active:focus,.show>.btn-success.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(72,180,97,.5)}.btn-info{color:#fff;background-color:#17a2b8;border-color:#17a2b8}.btn-info:hover{color:#fff;background-color:#138496;border-color:#117a8b}.btn-info.focus,.btn-info:focus{color:#fff;background-color:#138496;border-color:#117a8b;box-shadow:0 0 0 .2rem rgba(58,176,195,.5)}.btn-info.disabled,.btn-info:disabled{color:#fff;background-color:#17a2b8;border-color:#17a2b8}.btn-info:not(:disabled):not(.disabled).active,.btn-info:not(:disabled):not(.disabled):active,.show>.btn-info.dropdown-toggle{color:#fff;background-color:#117a8b;border-color:#10707f}.btn-info:not(:disabled):not(.disabled).active:focus,.btn-info:not(:disabled):not(.disabled):active:focus,.show>.btn-info.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(58,176,195,.5)}.btn-warning{color:#212529;background-color:#ffc107;border-color:#ffc107}.btn-warning:hover{color:#212529;background-color:#e0a800;border-color:#d39e00}.btn-warning.focus,.btn-warning:focus{color:#212529;background-color:#e0a800;border-color:#d39e00;box-shadow:0 0 0 .2rem rgba(222,170,12,.5)}.btn-warning.disabled,.btn-warning:disabled{color:#212529;background-color:#ffc107;border-color:#ffc107}.btn-warning:not(:disabled):not(.disabled).active,.btn-warning:not(:disabled):not(.disabled):active,.show>.btn-warning.dropdown-toggle{color:#212529;background-color:#d39e00;border-color:#c69500}.btn-warning:not(:disabled):not(.disabled).active:focus,.btn-warning:not(:disabled):not(.disabled):active:focus,.show>.btn-warning.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(222,170,12,.5)}.btn-danger{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-danger:hover{color:#fff;background-color:#c82333;border-color:#bd2130}.btn-danger.focus,.btn-danger:focus{color:#fff;background-color:#c82333;border-color:#bd2130;box-shadow:0 0 0 .2rem rgba(225,83,97,.5)}.btn-danger.disabled,.btn-danger:disabled{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-danger:not(:disabled):not(.disabled).active,.btn-danger:not(:disabled):not(.disabled):active,.show>.btn-danger.dropdown-toggle{color:#fff;background-color:#bd2130;border-color:#b21f2d}.btn-danger:not(:disabled):not(.disabled).active:focus,.btn-danger:not(:disabled):not(.disabled):active:focus,.show>.btn-danger.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(225,83,97,.5)}.btn-light{color:#212529;background-color:#f8f9fa;border-color:#f8f9fa}.btn-light:hover{color:#212529;background-color:#e2e6ea;border-color:#dae0e5}.btn-light.focus,.btn-light:focus{color:#212529;background-color:#e2e6ea;border-color:#dae0e5;box-shadow:0 0 0 .2rem rgba(216,217,219,.5)}.btn-light.disabled,.btn-light:disabled{color:#212529;background-color:#f8f9fa;border-color:#f8f9fa}.btn-light:not(:disabled):not(.disabled).active,.btn-light:not(:disabled):not(.disabled):active,.show>.btn-light.dropdown-toggle{color:#212529;background-color:#dae0e5;border-color:#d3d9df}.btn-light:not(:disabled):not(.disabled).active:focus,.btn-light:not(:disabled):not(.disabled):active:focus,.show>.btn-light.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(216,217,219,.5)}.btn-dark{color:#fff;background-color:#343a40;border-color:#343a40}.btn-dark:hover{color:#fff;background-color:#23272b;border-color:#1d2124}.btn-dark.focus,.btn-dark:focus{color:#fff;background-color:#23272b;border-color:#1d2124;box-shadow:0 0 0 .2rem rgba(82,88,93,.5)}.btn-dark.disabled,.btn-dark:disabled{color:#fff;background-color:#343a40;border-color:#343a40}.btn-dark:not(:disabled):not(.disabled).active,.btn-dark:not(:disabled):not(.disabled):active,.show>.btn-dark.dropdown-toggle{color:#fff;background-color:#1d2124;border-color:#171a1d}.btn-dark:not(:disabled):not(.disabled).active:focus,.btn-dark:not(:disabled):not(.disabled):active:focus,.show>.btn-dark.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(82,88,93,.5)}.btn-outline-primary{color:#007bff;border-color:#007bff}.btn-outline-primary:hover{color:#fff;background-color:#007bff;border-color:#007bff}.btn-outline-primary.focus,.btn-outline-primary:focus{box-shadow:0 0 0 .2rem rgba(0,123,255,.5)}.btn-outline-primary.disabled,.btn-outline-primary:disabled{color:#007bff;background-color:transparent}.btn-outline-primary:not(:disabled):not(.disabled).active,.btn-outline-primary:not(:disabled):not(.disabled):active,.show>.btn-outline-primary.dropdown-toggle{color:#fff;background-color:#007bff;border-color:#007bff}.btn-outline-primary:not(:disabled):not(.disabled).active:focus,.btn-outline-primary:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-primary.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(0,123,255,.5)}.btn-outline-secondary{color:#6c757d;border-color:#6c757d}.btn-outline-secondary:hover{color:#fff;background-color:#6c757d;border-color:#6c757d}.btn-outline-secondary.focus,.btn-outline-secondary:focus{box-shadow:0 0 0 .2rem rgba(108,117,125,.5)}.btn-outline-secondary.disabled,.btn-outline-secondary:disabled{color:#6c757d;background-color:transparent}.btn-outline-secondary:not(:disabled):not(.disabled).active,.btn-outline-secondary:not(:disabled):not(.disabled):active,.show>.btn-outline-secondary.dropdown-toggle{color:#fff;background-color:#6c757d;border-color:#6c757d}.btn-outline-secondary:not(:disabled):not(.disabled).active:focus,.btn-outline-secondary:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-secondary.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(108,117,125,.5)}.btn-outline-success{color:#28a745;border-color:#28a745}.btn-outline-success:hover{color:#fff;background-color:#28a745;border-color:#28a745}.btn-outline-success.focus,.btn-outline-success:focus{box-shadow:0 0 0 .2rem rgba(40,167,69,.5)}.btn-outline-success.disabled,.btn-outline-success:disabled{color:#28a745;background-color:transparent}.btn-outline-success:not(:disabled):not(.disabled).active,.btn-outline-success:not(:disabled):not(.disabled):active,.show>.btn-outline-success.dropdown-toggle{color:#fff;background-color:#28a745;border-color:#28a745}.btn-outline-success:not(:disabled):not(.disabled).active:focus,.btn-outline-success:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-success.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(40,167,69,.5)}.btn-outline-info{color:#17a2b8;border-color:#17a2b8}.btn-outline-info:hover{color:#fff;background-color:#17a2b8;border-color:#17a2b8}.btn-outline-info.focus,.btn-outline-info:focus{box-shadow:0 0 0 .2rem rgba(23,162,184,.5)}.btn-outline-info.disabled,.btn-outline-info:disabled{color:#17a2b8;background-color:transparent}.btn-outline-info:not(:disabled):not(.disabled).active,.btn-outline-info:not(:disabled):not(.disabled):active,.show>.btn-outline-info.dropdown-toggle{color:#fff;background-color:#17a2b8;border-color:#17a2b8}.btn-outline-info:not(:disabled):not(.disabled).active:focus,.btn-outline-info:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-info.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(23,162,184,.5)}.btn-outline-warning{color:#ffc107;border-color:#ffc107}.btn-outline-warning:hover{color:#212529;background-color:#ffc107;border-color:#ffc107}.btn-outline-warning.focus,.btn-outline-warning:focus{box-shadow:0 0 0 .2rem rgba(255,193,7,.5)}.btn-outline-warning.disabled,.btn-outline-warning:disabled{color:#ffc107;background-color:transparent}.btn-outline-warning:not(:disabled):not(.disabled).active,.btn-outline-warning:not(:disabled):not(.disabled):active,.show>.btn-outline-warning.dropdown-toggle{color:#212529;background-color:#ffc107;border-color:#ffc107}.btn-outline-warning:not(:disabled):not(.disabled).active:focus,.btn-outline-warning:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-warning.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(255,193,7,.5)}.btn-outline-danger{color:#dc3545;border-color:#dc3545}.btn-outline-danger:hover{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-outline-danger.focus,.btn-outline-danger:focus{box-shadow:0 0 0 .2rem rgba(220,53,69,.5)}.btn-outline-danger.disabled,.btn-outline-danger:disabled{color:#dc3545;background-color:transparent}.btn-outline-danger:not(:disabled):not(.disabled).active,.btn-outline-danger:not(:disabled):not(.disabled):active,.show>.btn-outline-danger.dropdown-toggle{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-outline-danger:not(:disabled):not(.disabled).active:focus,.btn-outline-danger:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-danger.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(220,53,69,.5)}.btn-outline-light{color:#f8f9fa;border-color:#f8f9fa}.btn-outline-light:hover{color:#212529;background-color:#f8f9fa;border-color:#f8f9fa}.btn-outline-light.focus,.btn-outline-light:focus{box-shadow:0 0 0 .2rem rgba(248,249,250,.5)}.btn-outline-light.disabled,.btn-outline-light:disabled{color:#f8f9fa;background-color:transparent}.btn-outline-light:not(:disabled):not(.disabled).active,.btn-outline-light:not(:disabled):not(.disabled):active,.show>.btn-outline-light.dropdown-toggle{color:#212529;background-color:#f8f9fa;border-color:#f8f9fa}.btn-outline-light:not(:disabled):not(.disabled).active:focus,.btn-outline-light:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-light.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(248,249,250,.5)}.btn-outline-dark{color:#343a40;border-color:#343a40}.btn-outline-dark:hover{color:#fff;background-color:#343a40;border-color:#343a40}.btn-outline-dark.focus,.btn-outline-dark:focus{box-shadow:0 0 0 .2rem rgba(52,58,64,.5)}.btn-outline-dark.disabled,.btn-outline-dark:disabled{color:#343a40;background-color:transparent}.btn-outline-dark:not(:disabled):not(.disabled).active,.btn-outline-dark:not(:disabled):not(.disabled):active,.show>.btn-outline-dark.dropdown-toggle{color:#fff;background-color:#343a40;border-color:#343a40}.btn-outline-dark:not(:disabled):not(.disabled).active:focus,.btn-outline-dark:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-dark.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(52,58,64,.5)}.btn-link{font-weight:400;color:#007bff;text-decoration:none}.btn-link:hover{color:#0056b3;text-decoration:underline}.btn-link.focus,.btn-link:focus{text-decoration:underline;box-shadow:none}.btn-link.disabled,.btn-link:disabled{color:#6c757d;pointer-events:none}.btn-group-lg>.btn,.btn-lg{padding:.5rem 1rem;font-size:1.25rem;line-height:1.5;border-radius:.3rem}.btn-group-sm>.btn,.btn-sm{padding:.25rem .5rem;font-size:.875rem;line-height:1.5;border-radius:.2rem}.btn-block{display:block;width:100%}.btn-block+.btn-block{margin-top:.5rem}input[type=button].btn-block,input[type=reset].btn-block,input[type=submit].btn-block{width:100%}.fade{transition:opacity .15s linear}@media (prefers-reduced-motion:reduce){.fade{transition:none}}.fade:not(.show){opacity:0}.collapse:not(.show){display:none}.collapsing{position:relative;height:0;overflow:hidden;transition:height .35s ease}@media (prefers-reduced-motion:reduce){.collapsing{transition:none}}.dropdown,.dropleft,.dropright,.dropup{position:relative}.dropdown-toggle{white-space:nowrap}.dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:\"\";border-top:.3em solid;border-right:.3em solid transparent;border-bottom:0;border-left:.3em solid transparent}.dropdown-toggle:empty::after{margin-left:0}.dropdown-menu{position:absolute;top:100%;left:0;z-index:1000;display:none;float:left;min-width:10rem;padding:.5rem 0;margin:.125rem 0 0;font-size:1rem;color:#212529;text-align:left;list-style:none;background-color:#fff;background-clip:padding-box;border:1px solid rgba(0,0,0,.15);border-radius:.25rem}.dropdown-menu-left{right:auto;left:0}.dropdown-menu-right{right:0;left:auto}@media (min-width:576px){.dropdown-menu-sm-left{right:auto;left:0}.dropdown-menu-sm-right{right:0;left:auto}}@media (min-width:768px){.dropdown-menu-md-left{right:auto;left:0}.dropdown-menu-md-right{right:0;left:auto}}@media (min-width:992px){.dropdown-menu-lg-left{right:auto;left:0}.dropdown-menu-lg-right{right:0;left:auto}}@media (min-width:1200px){.dropdown-menu-xl-left{right:auto;left:0}.dropdown-menu-xl-right{right:0;left:auto}}.dropup .dropdown-menu{top:auto;bottom:100%;margin-top:0;margin-bottom:.125rem}.dropup .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:\"\";border-top:0;border-right:.3em solid transparent;border-bottom:.3em solid;border-left:.3em solid transparent}.dropup .dropdown-toggle:empty::after{margin-left:0}.dropright .dropdown-menu{top:0;right:auto;left:100%;margin-top:0;margin-left:.125rem}.dropright .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:\"\";border-top:.3em solid transparent;border-right:0;border-bottom:.3em solid transparent;border-left:.3em solid}.dropright .dropdown-toggle:empty::after{margin-left:0}.dropright .dropdown-toggle::after{vertical-align:0}.dropleft .dropdown-menu{top:0;right:100%;left:auto;margin-top:0;margin-right:.125rem}.dropleft .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:\"\"}.dropleft .dropdown-toggle::after{display:none}.dropleft .dropdown-toggle::before{display:inline-block;margin-right:.255em;vertical-align:.255em;content:\"\";border-top:.3em solid transparent;border-right:.3em solid;border-bottom:.3em solid transparent}.dropleft .dropdown-toggle:empty::after{margin-left:0}.dropleft .dropdown-toggle::before{vertical-align:0}.dropdown-menu[x-placement^=bottom],.dropdown-menu[x-placement^=left],.dropdown-menu[x-placement^=right],.dropdown-menu[x-placement^=top]{right:auto;bottom:auto}.dropdown-divider{height:0;margin:.5rem 0;overflow:hidden;border-top:1px solid #e9ecef}.dropdown-item{display:block;width:100%;padding:.25rem 1.5rem;clear:both;font-weight:400;color:#212529;text-align:inherit;white-space:nowrap;background-color:transparent;border:0}.dropdown-item:focus,.dropdown-item:hover{color:#16181b;text-decoration:none;background-color:#f8f9fa}.dropdown-item.active,.dropdown-item:active{color:#fff;text-decoration:none;background-color:#007bff}.dropdown-item.disabled,.dropdown-item:disabled{color:#6c757d;pointer-events:none;background-color:transparent}.dropdown-menu.show{display:block}.dropdown-header{display:block;padding:.5rem 1.5rem;margin-bottom:0;font-size:.875rem;color:#6c757d;white-space:nowrap}.dropdown-item-text{display:block;padding:.25rem 1.5rem;color:#212529}.btn-group,.btn-group-vertical{position:relative;display:-ms-inline-flexbox;display:inline-flex;vertical-align:middle}.btn-group-vertical>.btn,.btn-group>.btn{position:relative;-ms-flex:1 1 auto;flex:1 1 auto}.btn-group-vertical>.btn:hover,.btn-group>.btn:hover{z-index:1}.btn-group-vertical>.btn.active,.btn-group-vertical>.btn:active,.btn-group-vertical>.btn:focus,.btn-group>.btn.active,.btn-group>.btn:active,.btn-group>.btn:focus{z-index:1}.btn-toolbar{display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;-ms-flex-pack:start;justify-content:flex-start}.btn-toolbar .input-group{width:auto}.btn-group>.btn-group:not(:first-child),.btn-group>.btn:not(:first-child){margin-left:-1px}.btn-group>.btn-group:not(:last-child)>.btn,.btn-group>.btn:not(:last-child):not(.dropdown-toggle){border-top-right-radius:0;border-bottom-right-radius:0}.btn-group>.btn-group:not(:first-child)>.btn,.btn-group>.btn:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0}.dropdown-toggle-split{padding-right:.5625rem;padding-left:.5625rem}.dropdown-toggle-split::after,.dropright .dropdown-toggle-split::after,.dropup .dropdown-toggle-split::after{margin-left:0}.dropleft .dropdown-toggle-split::before{margin-right:0}.btn-group-sm>.btn+.dropdown-toggle-split,.btn-sm+.dropdown-toggle-split{padding-right:.375rem;padding-left:.375rem}.btn-group-lg>.btn+.dropdown-toggle-split,.btn-lg+.dropdown-toggle-split{padding-right:.75rem;padding-left:.75rem}.btn-group-vertical{-ms-flex-direction:column;flex-direction:column;-ms-flex-align:start;align-items:flex-start;-ms-flex-pack:center;justify-content:center}.btn-group-vertical>.btn,.btn-group-vertical>.btn-group{width:100%}.btn-group-vertical>.btn-group:not(:first-child),.btn-group-vertical>.btn:not(:first-child){margin-top:-1px}.btn-group-vertical>.btn-group:not(:last-child)>.btn,.btn-group-vertical>.btn:not(:last-child):not(.dropdown-toggle){border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn-group:not(:first-child)>.btn,.btn-group-vertical>.btn:not(:first-child){border-top-left-radius:0;border-top-right-radius:0}.btn-group-toggle>.btn,.btn-group-toggle>.btn-group>.btn{margin-bottom:0}.btn-group-toggle>.btn input[type=checkbox],.btn-group-toggle>.btn input[type=radio],.btn-group-toggle>.btn-group>.btn input[type=checkbox],.btn-group-toggle>.btn-group>.btn input[type=radio]{position:absolute;clip:rect(0,0,0,0);pointer-events:none}.input-group{position:relative;display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;-ms-flex-align:stretch;align-items:stretch;width:100%}.input-group>.custom-file,.input-group>.custom-select,.input-group>.form-control,.input-group>.form-control-plaintext{position:relative;-ms-flex:1 1 0%;flex:1 1 0%;min-width:0;margin-bottom:0}.input-group>.custom-file+.custom-file,.input-group>.custom-file+.custom-select,.input-group>.custom-file+.form-control,.input-group>.custom-select+.custom-file,.input-group>.custom-select+.custom-select,.input-group>.custom-select+.form-control,.input-group>.form-control+.custom-file,.input-group>.form-control+.custom-select,.input-group>.form-control+.form-control,.input-group>.form-control-plaintext+.custom-file,.input-group>.form-control-plaintext+.custom-select,.input-group>.form-control-plaintext+.form-control{margin-left:-1px}.input-group>.custom-file .custom-file-input:focus~.custom-file-label,.input-group>.custom-select:focus,.input-group>.form-control:focus{z-index:3}.input-group>.custom-file .custom-file-input:focus{z-index:4}.input-group>.custom-select:not(:last-child),.input-group>.form-control:not(:last-child){border-top-right-radius:0;border-bottom-right-radius:0}.input-group>.custom-select:not(:first-child),.input-group>.form-control:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0}.input-group>.custom-file{display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center}.input-group>.custom-file:not(:last-child) .custom-file-label,.input-group>.custom-file:not(:last-child) .custom-file-label::after{border-top-right-radius:0;border-bottom-right-radius:0}.input-group>.custom-file:not(:first-child) .custom-file-label{border-top-left-radius:0;border-bottom-left-radius:0}.input-group-append,.input-group-prepend{display:-ms-flexbox;display:flex}.input-group-append .btn,.input-group-prepend .btn{position:relative;z-index:2}.input-group-append .btn:focus,.input-group-prepend .btn:focus{z-index:3}.input-group-append .btn+.btn,.input-group-append .btn+.input-group-text,.input-group-append .input-group-text+.btn,.input-group-append .input-group-text+.input-group-text,.input-group-prepend .btn+.btn,.input-group-prepend .btn+.input-group-text,.input-group-prepend .input-group-text+.btn,.input-group-prepend .input-group-text+.input-group-text{margin-left:-1px}.input-group-prepend{margin-right:-1px}.input-group-append{margin-left:-1px}.input-group-text{display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;padding:.375rem .75rem;margin-bottom:0;font-size:1rem;font-weight:400;line-height:1.5;color:#495057;text-align:center;white-space:nowrap;background-color:#e9ecef;border:1px solid #ced4da;border-radius:.25rem}.input-group-text input[type=checkbox],.input-group-text input[type=radio]{margin-top:0}.input-group-lg>.custom-select,.input-group-lg>.form-control:not(textarea){height:calc(1.5em + 1rem + 2px)}.input-group-lg>.custom-select,.input-group-lg>.form-control,.input-group-lg>.input-group-append>.btn,.input-group-lg>.input-group-append>.input-group-text,.input-group-lg>.input-group-prepend>.btn,.input-group-lg>.input-group-prepend>.input-group-text{padding:.5rem 1rem;font-size:1.25rem;line-height:1.5;border-radius:.3rem}.input-group-sm>.custom-select,.input-group-sm>.form-control:not(textarea){height:calc(1.5em + .5rem + 2px)}.input-group-sm>.custom-select,.input-group-sm>.form-control,.input-group-sm>.input-group-append>.btn,.input-group-sm>.input-group-append>.input-group-text,.input-group-sm>.input-group-prepend>.btn,.input-group-sm>.input-group-prepend>.input-group-text{padding:.25rem .5rem;font-size:.875rem;line-height:1.5;border-radius:.2rem}.input-group-lg>.custom-select,.input-group-sm>.custom-select{padding-right:1.75rem}.input-group>.input-group-append:last-child>.btn:not(:last-child):not(.dropdown-toggle),.input-group>.input-group-append:last-child>.input-group-text:not(:last-child),.input-group>.input-group-append:not(:last-child)>.btn,.input-group>.input-group-append:not(:last-child)>.input-group-text,.input-group>.input-group-prepend>.btn,.input-group>.input-group-prepend>.input-group-text{border-top-right-radius:0;border-bottom-right-radius:0}.input-group>.input-group-append>.btn,.input-group>.input-group-append>.input-group-text,.input-group>.input-group-prepend:first-child>.btn:not(:first-child),.input-group>.input-group-prepend:first-child>.input-group-text:not(:first-child),.input-group>.input-group-prepend:not(:first-child)>.btn,.input-group>.input-group-prepend:not(:first-child)>.input-group-text{border-top-left-radius:0;border-bottom-left-radius:0}.custom-control{position:relative;display:block;min-height:1.5rem;padding-left:1.5rem}.custom-control-inline{display:-ms-inline-flexbox;display:inline-flex;margin-right:1rem}.custom-control-input{position:absolute;left:0;z-index:-1;width:1rem;height:1.25rem;opacity:0}.custom-control-input:checked~.custom-control-label::before{color:#fff;border-color:#007bff;background-color:#007bff}.custom-control-input:focus~.custom-control-label::before{box-shadow:0 0 0 .2rem rgba(0,123,255,.25)}.custom-control-input:focus:not(:checked)~.custom-control-label::before{border-color:#80bdff}.custom-control-input:not(:disabled):active~.custom-control-label::before{color:#fff;background-color:#b3d7ff;border-color:#b3d7ff}.custom-control-input:disabled~.custom-control-label,.custom-control-input[disabled]~.custom-control-label{color:#6c757d}.custom-control-input:disabled~.custom-control-label::before,.custom-control-input[disabled]~.custom-control-label::before{background-color:#e9ecef}.custom-control-label{position:relative;margin-bottom:0;vertical-align:top}.custom-control-label::before{position:absolute;top:.25rem;left:-1.5rem;display:block;width:1rem;height:1rem;pointer-events:none;content:\"\";background-color:#fff;border:#adb5bd solid 1px}.custom-control-label::after{position:absolute;top:.25rem;left:-1.5rem;display:block;width:1rem;height:1rem;content:\"\";background:no-repeat 50%/50% 50%}.custom-checkbox .custom-control-label::before{border-radius:.25rem}.custom-checkbox .custom-control-input:checked~.custom-control-label::after{background-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='8' height='8' viewBox='0 0 8 8'%3e%3cpath fill='%23fff' d='M6.564.75l-3.59 3.612-1.538-1.55L0 4.26l2.974 2.99L8 2.193z'/%3e%3c/svg%3e\")}.custom-checkbox .custom-control-input:indeterminate~.custom-control-label::before{border-color:#007bff;background-color:#007bff}.custom-checkbox .custom-control-input:indeterminate~.custom-control-label::after{background-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='4' height='4' viewBox='0 0 4 4'%3e%3cpath stroke='%23fff' d='M0 2h4'/%3e%3c/svg%3e\")}.custom-checkbox .custom-control-input:disabled:checked~.custom-control-label::before{background-color:rgba(0,123,255,.5)}.custom-checkbox .custom-control-input:disabled:indeterminate~.custom-control-label::before{background-color:rgba(0,123,255,.5)}.custom-radio .custom-control-label::before{border-radius:50%}.custom-radio .custom-control-input:checked~.custom-control-label::after{background-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%23fff'/%3e%3c/svg%3e\")}.custom-radio .custom-control-input:disabled:checked~.custom-control-label::before{background-color:rgba(0,123,255,.5)}.custom-switch{padding-left:2.25rem}.custom-switch .custom-control-label::before{left:-2.25rem;width:1.75rem;pointer-events:all;border-radius:.5rem}.custom-switch .custom-control-label::after{top:calc(.25rem + 2px);left:calc(-2.25rem + 2px);width:calc(1rem - 4px);height:calc(1rem - 4px);background-color:#adb5bd;border-radius:.5rem;transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out,-webkit-transform .15s ease-in-out;transition:transform .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;transition:transform .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out,-webkit-transform .15s ease-in-out}@media (prefers-reduced-motion:reduce){.custom-switch .custom-control-label::after{transition:none}}.custom-switch .custom-control-input:checked~.custom-control-label::after{background-color:#fff;-webkit-transform:translateX(.75rem);transform:translateX(.75rem)}.custom-switch .custom-control-input:disabled:checked~.custom-control-label::before{background-color:rgba(0,123,255,.5)}.custom-select{display:inline-block;width:100%;height:calc(1.5em + .75rem + 2px);padding:.375rem 1.75rem .375rem .75rem;font-size:1rem;font-weight:400;line-height:1.5;color:#495057;vertical-align:middle;background:#fff url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='4' height='5' viewBox='0 0 4 5'%3e%3cpath fill='%23343a40' d='M2 0L0 2h4zm0 5L0 3h4z'/%3e%3c/svg%3e\") no-repeat right .75rem center/8px 10px;border:1px solid #ced4da;border-radius:.25rem;-webkit-appearance:none;-moz-appearance:none;appearance:none}.custom-select:focus{border-color:#80bdff;outline:0;box-shadow:0 0 0 .2rem rgba(0,123,255,.25)}.custom-select:focus::-ms-value{color:#495057;background-color:#fff}.custom-select[multiple],.custom-select[size]:not([size=\"1\"]){height:auto;padding-right:.75rem;background-image:none}.custom-select:disabled{color:#6c757d;background-color:#e9ecef}.custom-select::-ms-expand{display:none}.custom-select:-moz-focusring{color:transparent;text-shadow:0 0 0 #495057}.custom-select-sm{height:calc(1.5em + .5rem + 2px);padding-top:.25rem;padding-bottom:.25rem;padding-left:.5rem;font-size:.875rem}.custom-select-lg{height:calc(1.5em + 1rem + 2px);padding-top:.5rem;padding-bottom:.5rem;padding-left:1rem;font-size:1.25rem}.custom-file{position:relative;display:inline-block;width:100%;height:calc(1.5em + .75rem + 2px);margin-bottom:0}.custom-file-input{position:relative;z-index:2;width:100%;height:calc(1.5em + .75rem + 2px);margin:0;opacity:0}.custom-file-input:focus~.custom-file-label{border-color:#80bdff;box-shadow:0 0 0 .2rem rgba(0,123,255,.25)}.custom-file-input:disabled~.custom-file-label,.custom-file-input[disabled]~.custom-file-label{background-color:#e9ecef}.custom-file-input:lang(en)~.custom-file-label::after{content:\"Browse\"}.custom-file-input~.custom-file-label[data-browse]::after{content:attr(data-browse)}.custom-file-label{position:absolute;top:0;right:0;left:0;z-index:1;height:calc(1.5em + .75rem + 2px);padding:.375rem .75rem;font-weight:400;line-height:1.5;color:#495057;background-color:#fff;border:1px solid #ced4da;border-radius:.25rem}.custom-file-label::after{position:absolute;top:0;right:0;bottom:0;z-index:3;display:block;height:calc(1.5em + .75rem);padding:.375rem .75rem;line-height:1.5;color:#495057;content:\"Browse\";background-color:#e9ecef;border-left:inherit;border-radius:0 .25rem .25rem 0}.custom-range{width:100%;height:1.4rem;padding:0;background-color:transparent;-webkit-appearance:none;-moz-appearance:none;appearance:none}.custom-range:focus{outline:0}.custom-range:focus::-webkit-slider-thumb{box-shadow:0 0 0 1px #fff,0 0 0 .2rem rgba(0,123,255,.25)}.custom-range:focus::-moz-range-thumb{box-shadow:0 0 0 1px #fff,0 0 0 .2rem rgba(0,123,255,.25)}.custom-range:focus::-ms-thumb{box-shadow:0 0 0 1px #fff,0 0 0 .2rem rgba(0,123,255,.25)}.custom-range::-moz-focus-outer{border:0}.custom-range::-webkit-slider-thumb{width:1rem;height:1rem;margin-top:-.25rem;background-color:#007bff;border:0;border-radius:1rem;-webkit-transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;-webkit-appearance:none;appearance:none}@media (prefers-reduced-motion:reduce){.custom-range::-webkit-slider-thumb{-webkit-transition:none;transition:none}}.custom-range::-webkit-slider-thumb:active{background-color:#b3d7ff}.custom-range::-webkit-slider-runnable-track{width:100%;height:.5rem;color:transparent;cursor:pointer;background-color:#dee2e6;border-color:transparent;border-radius:1rem}.custom-range::-moz-range-thumb{width:1rem;height:1rem;background-color:#007bff;border:0;border-radius:1rem;-moz-transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;-moz-appearance:none;appearance:none}@media (prefers-reduced-motion:reduce){.custom-range::-moz-range-thumb{-moz-transition:none;transition:none}}.custom-range::-moz-range-thumb:active{background-color:#b3d7ff}.custom-range::-moz-range-track{width:100%;height:.5rem;color:transparent;cursor:pointer;background-color:#dee2e6;border-color:transparent;border-radius:1rem}.custom-range::-ms-thumb{width:1rem;height:1rem;margin-top:0;margin-right:.2rem;margin-left:.2rem;background-color:#007bff;border:0;border-radius:1rem;-ms-transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;appearance:none}@media (prefers-reduced-motion:reduce){.custom-range::-ms-thumb{-ms-transition:none;transition:none}}.custom-range::-ms-thumb:active{background-color:#b3d7ff}.custom-range::-ms-track{width:100%;height:.5rem;color:transparent;cursor:pointer;background-color:transparent;border-color:transparent;border-width:.5rem}.custom-range::-ms-fill-lower{background-color:#dee2e6;border-radius:1rem}.custom-range::-ms-fill-upper{margin-right:15px;background-color:#dee2e6;border-radius:1rem}.custom-range:disabled::-webkit-slider-thumb{background-color:#adb5bd}.custom-range:disabled::-webkit-slider-runnable-track{cursor:default}.custom-range:disabled::-moz-range-thumb{background-color:#adb5bd}.custom-range:disabled::-moz-range-track{cursor:default}.custom-range:disabled::-ms-thumb{background-color:#adb5bd}.custom-control-label::before,.custom-file-label,.custom-select{transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.custom-control-label::before,.custom-file-label,.custom-select{transition:none}}.nav{display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;padding-left:0;margin-bottom:0;list-style:none}.nav-link{display:block;padding:.5rem 1rem}.nav-link:focus,.nav-link:hover{text-decoration:none}.nav-link.disabled{color:#6c757d;pointer-events:none;cursor:default}.nav-tabs{border-bottom:1px solid #dee2e6}.nav-tabs .nav-item{margin-bottom:-1px}.nav-tabs .nav-link{border:1px solid transparent;border-top-left-radius:.25rem;border-top-right-radius:.25rem}.nav-tabs .nav-link:focus,.nav-tabs .nav-link:hover{border-color:#e9ecef #e9ecef #dee2e6}.nav-tabs .nav-link.disabled{color:#6c757d;background-color:transparent;border-color:transparent}.nav-tabs .nav-item.show .nav-link,.nav-tabs .nav-link.active{color:#495057;background-color:#fff;border-color:#dee2e6 #dee2e6 #fff}.nav-tabs .dropdown-menu{margin-top:-1px;border-top-left-radius:0;border-top-right-radius:0}.nav-pills .nav-link{border-radius:.25rem}.nav-pills .nav-link.active,.nav-pills .show>.nav-link{color:#fff;background-color:#007bff}.nav-fill .nav-item{-ms-flex:1 1 auto;flex:1 1 auto;text-align:center}.nav-justified .nav-item{-ms-flex-preferred-size:0;flex-basis:0;-ms-flex-positive:1;flex-grow:1;text-align:center}.tab-content>.tab-pane{display:none}.tab-content>.active{display:block}.navbar{position:relative;display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;-ms-flex-align:center;align-items:center;-ms-flex-pack:justify;justify-content:space-between;padding:.5rem 1rem}.navbar .container,.navbar .container-fluid,.navbar .container-lg,.navbar .container-md,.navbar .container-sm,.navbar .container-xl{display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;-ms-flex-align:center;align-items:center;-ms-flex-pack:justify;justify-content:space-between}.navbar-brand{display:inline-block;padding-top:.3125rem;padding-bottom:.3125rem;margin-right:1rem;font-size:1.25rem;line-height:inherit;white-space:nowrap}.navbar-brand:focus,.navbar-brand:hover{text-decoration:none}.navbar-nav{display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;padding-left:0;margin-bottom:0;list-style:none}.navbar-nav .nav-link{padding-right:0;padding-left:0}.navbar-nav .dropdown-menu{position:static;float:none}.navbar-text{display:inline-block;padding-top:.5rem;padding-bottom:.5rem}.navbar-collapse{-ms-flex-preferred-size:100%;flex-basis:100%;-ms-flex-positive:1;flex-grow:1;-ms-flex-align:center;align-items:center}.navbar-toggler{padding:.25rem .75rem;font-size:1.25rem;line-height:1;background-color:transparent;border:1px solid transparent;border-radius:.25rem}.navbar-toggler:focus,.navbar-toggler:hover{text-decoration:none}.navbar-toggler-icon{display:inline-block;width:1.5em;height:1.5em;vertical-align:middle;content:\"\";background:no-repeat center center;background-size:100% 100%}@media (max-width:575.98px){.navbar-expand-sm>.container,.navbar-expand-sm>.container-fluid,.navbar-expand-sm>.container-lg,.navbar-expand-sm>.container-md,.navbar-expand-sm>.container-sm,.navbar-expand-sm>.container-xl{padding-right:0;padding-left:0}}@media (min-width:576px){.navbar-expand-sm{-ms-flex-flow:row nowrap;flex-flow:row nowrap;-ms-flex-pack:start;justify-content:flex-start}.navbar-expand-sm .navbar-nav{-ms-flex-direction:row;flex-direction:row}.navbar-expand-sm .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-sm .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-sm>.container,.navbar-expand-sm>.container-fluid,.navbar-expand-sm>.container-lg,.navbar-expand-sm>.container-md,.navbar-expand-sm>.container-sm,.navbar-expand-sm>.container-xl{-ms-flex-wrap:nowrap;flex-wrap:nowrap}.navbar-expand-sm .navbar-collapse{display:-ms-flexbox!important;display:flex!important;-ms-flex-preferred-size:auto;flex-basis:auto}.navbar-expand-sm .navbar-toggler{display:none}}@media (max-width:767.98px){.navbar-expand-md>.container,.navbar-expand-md>.container-fluid,.navbar-expand-md>.container-lg,.navbar-expand-md>.container-md,.navbar-expand-md>.container-sm,.navbar-expand-md>.container-xl{padding-right:0;padding-left:0}}@media (min-width:768px){.navbar-expand-md{-ms-flex-flow:row nowrap;flex-flow:row nowrap;-ms-flex-pack:start;justify-content:flex-start}.navbar-expand-md .navbar-nav{-ms-flex-direction:row;flex-direction:row}.navbar-expand-md .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-md .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-md>.container,.navbar-expand-md>.container-fluid,.navbar-expand-md>.container-lg,.navbar-expand-md>.container-md,.navbar-expand-md>.container-sm,.navbar-expand-md>.container-xl{-ms-flex-wrap:nowrap;flex-wrap:nowrap}.navbar-expand-md .navbar-collapse{display:-ms-flexbox!important;display:flex!important;-ms-flex-preferred-size:auto;flex-basis:auto}.navbar-expand-md .navbar-toggler{display:none}}@media (max-width:991.98px){.navbar-expand-lg>.container,.navbar-expand-lg>.container-fluid,.navbar-expand-lg>.container-lg,.navbar-expand-lg>.container-md,.navbar-expand-lg>.container-sm,.navbar-expand-lg>.container-xl{padding-right:0;padding-left:0}}@media (min-width:992px){.navbar-expand-lg{-ms-flex-flow:row nowrap;flex-flow:row nowrap;-ms-flex-pack:start;justify-content:flex-start}.navbar-expand-lg .navbar-nav{-ms-flex-direction:row;flex-direction:row}.navbar-expand-lg .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-lg .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-lg>.container,.navbar-expand-lg>.container-fluid,.navbar-expand-lg>.container-lg,.navbar-expand-lg>.container-md,.navbar-expand-lg>.container-sm,.navbar-expand-lg>.container-xl{-ms-flex-wrap:nowrap;flex-wrap:nowrap}.navbar-expand-lg .navbar-collapse{display:-ms-flexbox!important;display:flex!important;-ms-flex-preferred-size:auto;flex-basis:auto}.navbar-expand-lg .navbar-toggler{display:none}}@media (max-width:1199.98px){.navbar-expand-xl>.container,.navbar-expand-xl>.container-fluid,.navbar-expand-xl>.container-lg,.navbar-expand-xl>.container-md,.navbar-expand-xl>.container-sm,.navbar-expand-xl>.container-xl{padding-right:0;padding-left:0}}@media (min-width:1200px){.navbar-expand-xl{-ms-flex-flow:row nowrap;flex-flow:row nowrap;-ms-flex-pack:start;justify-content:flex-start}.navbar-expand-xl .navbar-nav{-ms-flex-direction:row;flex-direction:row}.navbar-expand-xl .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-xl .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-xl>.container,.navbar-expand-xl>.container-fluid,.navbar-expand-xl>.container-lg,.navbar-expand-xl>.container-md,.navbar-expand-xl>.container-sm,.navbar-expand-xl>.container-xl{-ms-flex-wrap:nowrap;flex-wrap:nowrap}.navbar-expand-xl .navbar-collapse{display:-ms-flexbox!important;display:flex!important;-ms-flex-preferred-size:auto;flex-basis:auto}.navbar-expand-xl .navbar-toggler{display:none}}.navbar-expand{-ms-flex-flow:row nowrap;flex-flow:row nowrap;-ms-flex-pack:start;justify-content:flex-start}.navbar-expand>.container,.navbar-expand>.container-fluid,.navbar-expand>.container-lg,.navbar-expand>.container-md,.navbar-expand>.container-sm,.navbar-expand>.container-xl{padding-right:0;padding-left:0}.navbar-expand .navbar-nav{-ms-flex-direction:row;flex-direction:row}.navbar-expand .navbar-nav .dropdown-menu{position:absolute}.navbar-expand .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand>.container,.navbar-expand>.container-fluid,.navbar-expand>.container-lg,.navbar-expand>.container-md,.navbar-expand>.container-sm,.navbar-expand>.container-xl{-ms-flex-wrap:nowrap;flex-wrap:nowrap}.navbar-expand .navbar-collapse{display:-ms-flexbox!important;display:flex!important;-ms-flex-preferred-size:auto;flex-basis:auto}.navbar-expand .navbar-toggler{display:none}.navbar-light .navbar-brand{color:rgba(0,0,0,.9)}.navbar-light .navbar-brand:focus,.navbar-light .navbar-brand:hover{color:rgba(0,0,0,.9)}.navbar-light .navbar-nav .nav-link{color:rgba(0,0,0,.5)}.navbar-light .navbar-nav .nav-link:focus,.navbar-light .navbar-nav .nav-link:hover{color:rgba(0,0,0,.7)}.navbar-light .navbar-nav .nav-link.disabled{color:rgba(0,0,0,.3)}.navbar-light .navbar-nav .active>.nav-link,.navbar-light .navbar-nav .nav-link.active,.navbar-light .navbar-nav .nav-link.show,.navbar-light .navbar-nav .show>.nav-link{color:rgba(0,0,0,.9)}.navbar-light .navbar-toggler{color:rgba(0,0,0,.5);border-color:rgba(0,0,0,.1)}.navbar-light .navbar-toggler-icon{background-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='30' height='30' viewBox='0 0 30 30'%3e%3cpath stroke='rgba(0, 0, 0, 0.5)' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e\")}.navbar-light .navbar-text{color:rgba(0,0,0,.5)}.navbar-light .navbar-text a{color:rgba(0,0,0,.9)}.navbar-light .navbar-text a:focus,.navbar-light .navbar-text a:hover{color:rgba(0,0,0,.9)}.navbar-dark .navbar-brand{color:#fff}.navbar-dark .navbar-brand:focus,.navbar-dark .navbar-brand:hover{color:#fff}.navbar-dark .navbar-nav .nav-link{color:rgba(255,255,255,.5)}.navbar-dark .navbar-nav .nav-link:focus,.navbar-dark .navbar-nav .nav-link:hover{color:rgba(255,255,255,.75)}.navbar-dark .navbar-nav .nav-link.disabled{color:rgba(255,255,255,.25)}.navbar-dark .navbar-nav .active>.nav-link,.navbar-dark .navbar-nav .nav-link.active,.navbar-dark .navbar-nav .nav-link.show,.navbar-dark .navbar-nav .show>.nav-link{color:#fff}.navbar-dark .navbar-toggler{color:rgba(255,255,255,.5);border-color:rgba(255,255,255,.1)}.navbar-dark .navbar-toggler-icon{background-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='30' height='30' viewBox='0 0 30 30'%3e%3cpath stroke='rgba(255, 255, 255, 0.5)' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e\")}.navbar-dark .navbar-text{color:rgba(255,255,255,.5)}.navbar-dark .navbar-text a{color:#fff}.navbar-dark .navbar-text a:focus,.navbar-dark .navbar-text a:hover{color:#fff}.card{position:relative;display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;min-width:0;word-wrap:break-word;background-color:#fff;background-clip:border-box;border:1px solid rgba(0,0,0,.125);border-radius:.25rem}.card>hr{margin-right:0;margin-left:0}.card>.list-group:first-child .list-group-item:first-child{border-top-left-radius:.25rem;border-top-right-radius:.25rem}.card>.list-group:last-child .list-group-item:last-child{border-bottom-right-radius:.25rem;border-bottom-left-radius:.25rem}.card-body{-ms-flex:1 1 auto;flex:1 1 auto;min-height:1px;padding:1.25rem}.card-title{margin-bottom:.75rem}.card-subtitle{margin-top:-.375rem;margin-bottom:0}.card-text:last-child{margin-bottom:0}.card-link:hover{text-decoration:none}.card-link+.card-link{margin-left:1.25rem}.card-header{padding:.75rem 1.25rem;margin-bottom:0;background-color:rgba(0,0,0,.03);border-bottom:1px solid rgba(0,0,0,.125)}.card-header:first-child{border-radius:calc(.25rem - 1px) calc(.25rem - 1px) 0 0}.card-header+.list-group .list-group-item:first-child{border-top:0}.card-footer{padding:.75rem 1.25rem;background-color:rgba(0,0,0,.03);border-top:1px solid rgba(0,0,0,.125)}.card-footer:last-child{border-radius:0 0 calc(.25rem - 1px) calc(.25rem - 1px)}.card-header-tabs{margin-right:-.625rem;margin-bottom:-.75rem;margin-left:-.625rem;border-bottom:0}.card-header-pills{margin-right:-.625rem;margin-left:-.625rem}.card-img-overlay{position:absolute;top:0;right:0;bottom:0;left:0;padding:1.25rem}.card-img,.card-img-bottom,.card-img-top{-ms-flex-negative:0;flex-shrink:0;width:100%}.card-img,.card-img-top{border-top-left-radius:calc(.25rem - 1px);border-top-right-radius:calc(.25rem - 1px)}.card-img,.card-img-bottom{border-bottom-right-radius:calc(.25rem - 1px);border-bottom-left-radius:calc(.25rem - 1px)}.card-deck .card{margin-bottom:15px}@media (min-width:576px){.card-deck{display:-ms-flexbox;display:flex;-ms-flex-flow:row wrap;flex-flow:row wrap;margin-right:-15px;margin-left:-15px}.card-deck .card{-ms-flex:1 0 0%;flex:1 0 0%;margin-right:15px;margin-bottom:0;margin-left:15px}}.card-group>.card{margin-bottom:15px}@media (min-width:576px){.card-group{display:-ms-flexbox;display:flex;-ms-flex-flow:row wrap;flex-flow:row wrap}.card-group>.card{-ms-flex:1 0 0%;flex:1 0 0%;margin-bottom:0}.card-group>.card+.card{margin-left:0;border-left:0}.card-group>.card:not(:last-child){border-top-right-radius:0;border-bottom-right-radius:0}.card-group>.card:not(:last-child) .card-header,.card-group>.card:not(:last-child) .card-img-top{border-top-right-radius:0}.card-group>.card:not(:last-child) .card-footer,.card-group>.card:not(:last-child) .card-img-bottom{border-bottom-right-radius:0}.card-group>.card:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0}.card-group>.card:not(:first-child) .card-header,.card-group>.card:not(:first-child) .card-img-top{border-top-left-radius:0}.card-group>.card:not(:first-child) .card-footer,.card-group>.card:not(:first-child) .card-img-bottom{border-bottom-left-radius:0}}.card-columns .card{margin-bottom:.75rem}@media (min-width:576px){.card-columns{-webkit-column-count:3;-moz-column-count:3;column-count:3;-webkit-column-gap:1.25rem;-moz-column-gap:1.25rem;column-gap:1.25rem;orphans:1;widows:1}.card-columns .card{display:inline-block;width:100%}}.accordion>.card{overflow:hidden}.accordion>.card:not(:last-of-type){border-bottom:0;border-bottom-right-radius:0;border-bottom-left-radius:0}.accordion>.card:not(:first-of-type){border-top-left-radius:0;border-top-right-radius:0}.accordion>.card>.card-header{border-radius:0;margin-bottom:-1px}.breadcrumb{display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;padding:.75rem 1rem;margin-bottom:1rem;list-style:none;background-color:#e9ecef;border-radius:.25rem}.breadcrumb-item+.breadcrumb-item{padding-left:.5rem}.breadcrumb-item+.breadcrumb-item::before{display:inline-block;padding-right:.5rem;color:#6c757d;content:\"/\"}.breadcrumb-item+.breadcrumb-item:hover::before{text-decoration:underline}.breadcrumb-item+.breadcrumb-item:hover::before{text-decoration:none}.breadcrumb-item.active{color:#6c757d}.pagination{display:-ms-flexbox;display:flex;padding-left:0;list-style:none;border-radius:.25rem}.page-link{position:relative;display:block;padding:.5rem .75rem;margin-left:-1px;line-height:1.25;color:#007bff;background-color:#fff;border:1px solid #dee2e6}.page-link:hover{z-index:2;color:#0056b3;text-decoration:none;background-color:#e9ecef;border-color:#dee2e6}.page-link:focus{z-index:3;outline:0;box-shadow:0 0 0 .2rem rgba(0,123,255,.25)}.page-item:first-child .page-link{margin-left:0;border-top-left-radius:.25rem;border-bottom-left-radius:.25rem}.page-item:last-child .page-link{border-top-right-radius:.25rem;border-bottom-right-radius:.25rem}.page-item.active .page-link{z-index:3;color:#fff;background-color:#007bff;border-color:#007bff}.page-item.disabled .page-link{color:#6c757d;pointer-events:none;cursor:auto;background-color:#fff;border-color:#dee2e6}.pagination-lg .page-link{padding:.75rem 1.5rem;font-size:1.25rem;line-height:1.5}.pagination-lg .page-item:first-child .page-link{border-top-left-radius:.3rem;border-bottom-left-radius:.3rem}.pagination-lg .page-item:last-child .page-link{border-top-right-radius:.3rem;border-bottom-right-radius:.3rem}.pagination-sm .page-link{padding:.25rem .5rem;font-size:.875rem;line-height:1.5}.pagination-sm .page-item:first-child .page-link{border-top-left-radius:.2rem;border-bottom-left-radius:.2rem}.pagination-sm .page-item:last-child .page-link{border-top-right-radius:.2rem;border-bottom-right-radius:.2rem}.badge{display:inline-block;padding:.25em .4em;font-size:75%;font-weight:700;line-height:1;text-align:center;white-space:nowrap;vertical-align:baseline;border-radius:.25rem;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.badge{transition:none}}a.badge:focus,a.badge:hover{text-decoration:none}.badge:empty{display:none}.btn .badge{position:relative;top:-1px}.badge-pill{padding-right:.6em;padding-left:.6em;border-radius:10rem}.badge-primary{color:#fff;background-color:#007bff}a.badge-primary:focus,a.badge-primary:hover{color:#fff;background-color:#0062cc}a.badge-primary.focus,a.badge-primary:focus{outline:0;box-shadow:0 0 0 .2rem rgba(0,123,255,.5)}.badge-secondary{color:#fff;background-color:#6c757d}a.badge-secondary:focus,a.badge-secondary:hover{color:#fff;background-color:#545b62}a.badge-secondary.focus,a.badge-secondary:focus{outline:0;box-shadow:0 0 0 .2rem rgba(108,117,125,.5)}.badge-success{color:#fff;background-color:#28a745}a.badge-success:focus,a.badge-success:hover{color:#fff;background-color:#1e7e34}a.badge-success.focus,a.badge-success:focus{outline:0;box-shadow:0 0 0 .2rem rgba(40,167,69,.5)}.badge-info{color:#fff;background-color:#17a2b8}a.badge-info:focus,a.badge-info:hover{color:#fff;background-color:#117a8b}a.badge-info.focus,a.badge-info:focus{outline:0;box-shadow:0 0 0 .2rem rgba(23,162,184,.5)}.badge-warning{color:#212529;background-color:#ffc107}a.badge-warning:focus,a.badge-warning:hover{color:#212529;background-color:#d39e00}a.badge-warning.focus,a.badge-warning:focus{outline:0;box-shadow:0 0 0 .2rem rgba(255,193,7,.5)}.badge-danger{color:#fff;background-color:#dc3545}a.badge-danger:focus,a.badge-danger:hover{color:#fff;background-color:#bd2130}a.badge-danger.focus,a.badge-danger:focus{outline:0;box-shadow:0 0 0 .2rem rgba(220,53,69,.5)}.badge-light{color:#212529;background-color:#f8f9fa}a.badge-light:focus,a.badge-light:hover{color:#212529;background-color:#dae0e5}a.badge-light.focus,a.badge-light:focus{outline:0;box-shadow:0 0 0 .2rem rgba(248,249,250,.5)}.badge-dark{color:#fff;background-color:#343a40}a.badge-dark:focus,a.badge-dark:hover{color:#fff;background-color:#1d2124}a.badge-dark.focus,a.badge-dark:focus{outline:0;box-shadow:0 0 0 .2rem rgba(52,58,64,.5)}.jumbotron{padding:2rem 1rem;margin-bottom:2rem;background-color:#e9ecef;border-radius:.3rem}@media (min-width:576px){.jumbotron{padding:4rem 2rem}}.jumbotron-fluid{padding-right:0;padding-left:0;border-radius:0}.alert{position:relative;padding:.75rem 1.25rem;margin-bottom:1rem;border:1px solid transparent;border-radius:.25rem}.alert-heading{color:inherit}.alert-link{font-weight:700}.alert-dismissible{padding-right:4rem}.alert-dismissible .close{position:absolute;top:0;right:0;padding:.75rem 1.25rem;color:inherit}.alert-primary{color:#004085;background-color:#cce5ff;border-color:#b8daff}.alert-primary hr{border-top-color:#9fcdff}.alert-primary .alert-link{color:#002752}.alert-secondary{color:#383d41;background-color:#e2e3e5;border-color:#d6d8db}.alert-secondary hr{border-top-color:#c8cbcf}.alert-secondary .alert-link{color:#202326}.alert-success{color:#155724;background-color:#d4edda;border-color:#c3e6cb}.alert-success hr{border-top-color:#b1dfbb}.alert-success .alert-link{color:#0b2e13}.alert-info{color:#0c5460;background-color:#d1ecf1;border-color:#bee5eb}.alert-info hr{border-top-color:#abdde5}.alert-info .alert-link{color:#062c33}.alert-warning{color:#856404;background-color:#fff3cd;border-color:#ffeeba}.alert-warning hr{border-top-color:#ffe8a1}.alert-warning .alert-link{color:#533f03}.alert-danger{color:#721c24;background-color:#f8d7da;border-color:#f5c6cb}.alert-danger hr{border-top-color:#f1b0b7}.alert-danger .alert-link{color:#491217}.alert-light{color:#818182;background-color:#fefefe;border-color:#fdfdfe}.alert-light hr{border-top-color:#ececf6}.alert-light .alert-link{color:#686868}.alert-dark{color:#1b1e21;background-color:#d6d8d9;border-color:#c6c8ca}.alert-dark hr{border-top-color:#b9bbbe}.alert-dark .alert-link{color:#040505}@-webkit-keyframes progress-bar-stripes{from{background-position:1rem 0}to{background-position:0 0}}@keyframes progress-bar-stripes{from{background-position:1rem 0}to{background-position:0 0}}.progress{display:-ms-flexbox;display:flex;height:1rem;overflow:hidden;font-size:.75rem;background-color:#e9ecef;border-radius:.25rem}.progress-bar{display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;-ms-flex-pack:center;justify-content:center;overflow:hidden;color:#fff;text-align:center;white-space:nowrap;background-color:#007bff;transition:width .6s ease}@media (prefers-reduced-motion:reduce){.progress-bar{transition:none}}.progress-bar-striped{background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-size:1rem 1rem}.progress-bar-animated{-webkit-animation:progress-bar-stripes 1s linear infinite;animation:progress-bar-stripes 1s linear infinite}@media (prefers-reduced-motion:reduce){.progress-bar-animated{-webkit-animation:none;animation:none}}.media{display:-ms-flexbox;display:flex;-ms-flex-align:start;align-items:flex-start}.media-body{-ms-flex:1;flex:1}.list-group{display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;padding-left:0;margin-bottom:0}.list-group-item-action{width:100%;color:#495057;text-align:inherit}.list-group-item-action:focus,.list-group-item-action:hover{z-index:1;color:#495057;text-decoration:none;background-color:#f8f9fa}.list-group-item-action:active{color:#212529;background-color:#e9ecef}.list-group-item{position:relative;display:block;padding:.75rem 1.25rem;background-color:#fff;border:1px solid rgba(0,0,0,.125)}.list-group-item:first-child{border-top-left-radius:.25rem;border-top-right-radius:.25rem}.list-group-item:last-child{border-bottom-right-radius:.25rem;border-bottom-left-radius:.25rem}.list-group-item.disabled,.list-group-item:disabled{color:#6c757d;pointer-events:none;background-color:#fff}.list-group-item.active{z-index:2;color:#fff;background-color:#007bff;border-color:#007bff}.list-group-item+.list-group-item{border-top-width:0}.list-group-item+.list-group-item.active{margin-top:-1px;border-top-width:1px}.list-group-horizontal{-ms-flex-direction:row;flex-direction:row}.list-group-horizontal .list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal .list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal .list-group-item.active{margin-top:0}.list-group-horizontal .list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal .list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}@media (min-width:576px){.list-group-horizontal-sm{-ms-flex-direction:row;flex-direction:row}.list-group-horizontal-sm .list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-sm .list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal-sm .list-group-item.active{margin-top:0}.list-group-horizontal-sm .list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-sm .list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}@media (min-width:768px){.list-group-horizontal-md{-ms-flex-direction:row;flex-direction:row}.list-group-horizontal-md .list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-md .list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal-md .list-group-item.active{margin-top:0}.list-group-horizontal-md .list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-md .list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}@media (min-width:992px){.list-group-horizontal-lg{-ms-flex-direction:row;flex-direction:row}.list-group-horizontal-lg .list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-lg .list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal-lg .list-group-item.active{margin-top:0}.list-group-horizontal-lg .list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-lg .list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}@media (min-width:1200px){.list-group-horizontal-xl{-ms-flex-direction:row;flex-direction:row}.list-group-horizontal-xl .list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-xl .list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal-xl .list-group-item.active{margin-top:0}.list-group-horizontal-xl .list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-xl .list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}.list-group-flush .list-group-item{border-right-width:0;border-left-width:0;border-radius:0}.list-group-flush .list-group-item:first-child{border-top-width:0}.list-group-flush:last-child .list-group-item:last-child{border-bottom-width:0}.list-group-item-primary{color:#004085;background-color:#b8daff}.list-group-item-primary.list-group-item-action:focus,.list-group-item-primary.list-group-item-action:hover{color:#004085;background-color:#9fcdff}.list-group-item-primary.list-group-item-action.active{color:#fff;background-color:#004085;border-color:#004085}.list-group-item-secondary{color:#383d41;background-color:#d6d8db}.list-group-item-secondary.list-group-item-action:focus,.list-group-item-secondary.list-group-item-action:hover{color:#383d41;background-color:#c8cbcf}.list-group-item-secondary.list-group-item-action.active{color:#fff;background-color:#383d41;border-color:#383d41}.list-group-item-success{color:#155724;background-color:#c3e6cb}.list-group-item-success.list-group-item-action:focus,.list-group-item-success.list-group-item-action:hover{color:#155724;background-color:#b1dfbb}.list-group-item-success.list-group-item-action.active{color:#fff;background-color:#155724;border-color:#155724}.list-group-item-info{color:#0c5460;background-color:#bee5eb}.list-group-item-info.list-group-item-action:focus,.list-group-item-info.list-group-item-action:hover{color:#0c5460;background-color:#abdde5}.list-group-item-info.list-group-item-action.active{color:#fff;background-color:#0c5460;border-color:#0c5460}.list-group-item-warning{color:#856404;background-color:#ffeeba}.list-group-item-warning.list-group-item-action:focus,.list-group-item-warning.list-group-item-action:hover{color:#856404;background-color:#ffe8a1}.list-group-item-warning.list-group-item-action.active{color:#fff;background-color:#856404;border-color:#856404}.list-group-item-danger{color:#721c24;background-color:#f5c6cb}.list-group-item-danger.list-group-item-action:focus,.list-group-item-danger.list-group-item-action:hover{color:#721c24;background-color:#f1b0b7}.list-group-item-danger.list-group-item-action.active{color:#fff;background-color:#721c24;border-color:#721c24}.list-group-item-light{color:#818182;background-color:#fdfdfe}.list-group-item-light.list-group-item-action:focus,.list-group-item-light.list-group-item-action:hover{color:#818182;background-color:#ececf6}.list-group-item-light.list-group-item-action.active{color:#fff;background-color:#818182;border-color:#818182}.list-group-item-dark{color:#1b1e21;background-color:#c6c8ca}.list-group-item-dark.list-group-item-action:focus,.list-group-item-dark.list-group-item-action:hover{color:#1b1e21;background-color:#b9bbbe}.list-group-item-dark.list-group-item-action.active{color:#fff;background-color:#1b1e21;border-color:#1b1e21}.close{float:right;font-size:1.5rem;font-weight:700;line-height:1;color:#000;text-shadow:0 1px 0 #fff;opacity:.5}.close:hover{color:#000;text-decoration:none}.close:not(:disabled):not(.disabled):focus,.close:not(:disabled):not(.disabled):hover{opacity:.75}button.close{padding:0;background-color:transparent;border:0;-webkit-appearance:none;-moz-appearance:none;appearance:none}a.close.disabled{pointer-events:none}.toast{max-width:350px;overflow:hidden;font-size:.875rem;background-color:rgba(255,255,255,.85);background-clip:padding-box;border:1px solid rgba(0,0,0,.1);box-shadow:0 .25rem .75rem rgba(0,0,0,.1);-webkit-backdrop-filter:blur(10px);backdrop-filter:blur(10px);opacity:0;border-radius:.25rem}.toast:not(:last-child){margin-bottom:.75rem}.toast.showing{opacity:1}.toast.show{display:block;opacity:1}.toast.hide{display:none}.toast-header{display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;padding:.25rem .75rem;color:#6c757d;background-color:rgba(255,255,255,.85);background-clip:padding-box;border-bottom:1px solid rgba(0,0,0,.05)}.toast-body{padding:.75rem}.modal-open{overflow:hidden}.modal-open .modal{overflow-x:hidden;overflow-y:auto}.modal{position:fixed;top:0;left:0;z-index:1050;display:none;width:100%;height:100%;overflow:hidden;outline:0}.modal-dialog{position:relative;width:auto;margin:.5rem;pointer-events:none}.modal.fade .modal-dialog{transition:-webkit-transform .3s ease-out;transition:transform .3s ease-out;transition:transform .3s ease-out,-webkit-transform .3s ease-out;-webkit-transform:translate(0,-50px);transform:translate(0,-50px)}@media (prefers-reduced-motion:reduce){.modal.fade .modal-dialog{transition:none}}.modal.show .modal-dialog{-webkit-transform:none;transform:none}.modal.modal-static .modal-dialog{-webkit-transform:scale(1.02);transform:scale(1.02)}.modal-dialog-scrollable{display:-ms-flexbox;display:flex;max-height:calc(100% - 1rem)}.modal-dialog-scrollable .modal-content{max-height:calc(100vh - 1rem);overflow:hidden}.modal-dialog-scrollable .modal-footer,.modal-dialog-scrollable .modal-header{-ms-flex-negative:0;flex-shrink:0}.modal-dialog-scrollable .modal-body{overflow-y:auto}.modal-dialog-centered{display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;min-height:calc(100% - 1rem)}.modal-dialog-centered::before{display:block;height:calc(100vh - 1rem);content:\"\"}.modal-dialog-centered.modal-dialog-scrollable{-ms-flex-direction:column;flex-direction:column;-ms-flex-pack:center;justify-content:center;height:100%}.modal-dialog-centered.modal-dialog-scrollable .modal-content{max-height:none}.modal-dialog-centered.modal-dialog-scrollable::before{content:none}.modal-content{position:relative;display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;width:100%;pointer-events:auto;background-color:#fff;background-clip:padding-box;border:1px solid rgba(0,0,0,.2);border-radius:.3rem;outline:0}.modal-backdrop{position:fixed;top:0;left:0;z-index:1040;width:100vw;height:100vh;background-color:#000}.modal-backdrop.fade{opacity:0}.modal-backdrop.show{opacity:.5}.modal-header{display:-ms-flexbox;display:flex;-ms-flex-align:start;align-items:flex-start;-ms-flex-pack:justify;justify-content:space-between;padding:1rem 1rem;border-bottom:1px solid #dee2e6;border-top-left-radius:calc(.3rem - 1px);border-top-right-radius:calc(.3rem - 1px)}.modal-header .close{padding:1rem 1rem;margin:-1rem -1rem -1rem auto}.modal-title{margin-bottom:0;line-height:1.5}.modal-body{position:relative;-ms-flex:1 1 auto;flex:1 1 auto;padding:1rem}.modal-footer{display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;-ms-flex-align:center;align-items:center;-ms-flex-pack:end;justify-content:flex-end;padding:.75rem;border-top:1px solid #dee2e6;border-bottom-right-radius:calc(.3rem - 1px);border-bottom-left-radius:calc(.3rem - 1px)}.modal-footer>*{margin:.25rem}.modal-scrollbar-measure{position:absolute;top:-9999px;width:50px;height:50px;overflow:scroll}@media (min-width:576px){.modal-dialog{max-width:500px;margin:1.75rem auto}.modal-dialog-scrollable{max-height:calc(100% - 3.5rem)}.modal-dialog-scrollable .modal-content{max-height:calc(100vh - 3.5rem)}.modal-dialog-centered{min-height:calc(100% - 3.5rem)}.modal-dialog-centered::before{height:calc(100vh - 3.5rem)}.modal-sm{max-width:300px}}@media (min-width:992px){.modal-lg,.modal-xl{max-width:800px}}@media (min-width:1200px){.modal-xl{max-width:1140px}}.tooltip{position:absolute;z-index:1070;display:block;margin:0;font-family:-apple-system,BlinkMacSystemFont,\"Segoe UI\",Roboto,\"Helvetica Neue\",Arial,\"Noto Sans\",sans-serif,\"Apple Color Emoji\",\"Segoe UI Emoji\",\"Segoe UI Symbol\",\"Noto Color Emoji\";font-style:normal;font-weight:400;line-height:1.5;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;word-spacing:normal;white-space:normal;line-break:auto;font-size:.875rem;word-wrap:break-word;opacity:0}.tooltip.show{opacity:.9}.tooltip .arrow{position:absolute;display:block;width:.8rem;height:.4rem}.tooltip .arrow::before{position:absolute;content:\"\";border-color:transparent;border-style:solid}.bs-tooltip-auto[x-placement^=top],.bs-tooltip-top{padding:.4rem 0}.bs-tooltip-auto[x-placement^=top] .arrow,.bs-tooltip-top .arrow{bottom:0}.bs-tooltip-auto[x-placement^=top] .arrow::before,.bs-tooltip-top .arrow::before{top:0;border-width:.4rem .4rem 0;border-top-color:#000}.bs-tooltip-auto[x-placement^=right],.bs-tooltip-right{padding:0 .4rem}.bs-tooltip-auto[x-placement^=right] .arrow,.bs-tooltip-right .arrow{left:0;width:.4rem;height:.8rem}.bs-tooltip-auto[x-placement^=right] .arrow::before,.bs-tooltip-right .arrow::before{right:0;border-width:.4rem .4rem .4rem 0;border-right-color:#000}.bs-tooltip-auto[x-placement^=bottom],.bs-tooltip-bottom{padding:.4rem 0}.bs-tooltip-auto[x-placement^=bottom] .arrow,.bs-tooltip-bottom .arrow{top:0}.bs-tooltip-auto[x-placement^=bottom] .arrow::before,.bs-tooltip-bottom .arrow::before{bottom:0;border-width:0 .4rem .4rem;border-bottom-color:#000}.bs-tooltip-auto[x-placement^=left],.bs-tooltip-left{padding:0 .4rem}.bs-tooltip-auto[x-placement^=left] .arrow,.bs-tooltip-left .arrow{right:0;width:.4rem;height:.8rem}.bs-tooltip-auto[x-placement^=left] .arrow::before,.bs-tooltip-left .arrow::before{left:0;border-width:.4rem 0 .4rem .4rem;border-left-color:#000}.tooltip-inner{max-width:200px;padding:.25rem .5rem;color:#fff;text-align:center;background-color:#000;border-radius:.25rem}.popover{position:absolute;top:0;left:0;z-index:1060;display:block;max-width:276px;font-family:-apple-system,BlinkMacSystemFont,\"Segoe UI\",Roboto,\"Helvetica Neue\",Arial,\"Noto Sans\",sans-serif,\"Apple Color Emoji\",\"Segoe UI Emoji\",\"Segoe UI Symbol\",\"Noto Color Emoji\";font-style:normal;font-weight:400;line-height:1.5;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;word-spacing:normal;white-space:normal;line-break:auto;font-size:.875rem;word-wrap:break-word;background-color:#fff;background-clip:padding-box;border:1px solid rgba(0,0,0,.2);border-radius:.3rem}.popover .arrow{position:absolute;display:block;width:1rem;height:.5rem;margin:0 .3rem}.popover .arrow::after,.popover .arrow::before{position:absolute;display:block;content:\"\";border-color:transparent;border-style:solid}.bs-popover-auto[x-placement^=top],.bs-popover-top{margin-bottom:.5rem}.bs-popover-auto[x-placement^=top]>.arrow,.bs-popover-top>.arrow{bottom:calc(-.5rem - 1px)}.bs-popover-auto[x-placement^=top]>.arrow::before,.bs-popover-top>.arrow::before{bottom:0;border-width:.5rem .5rem 0;border-top-color:rgba(0,0,0,.25)}.bs-popover-auto[x-placement^=top]>.arrow::after,.bs-popover-top>.arrow::after{bottom:1px;border-width:.5rem .5rem 0;border-top-color:#fff}.bs-popover-auto[x-placement^=right],.bs-popover-right{margin-left:.5rem}.bs-popover-auto[x-placement^=right]>.arrow,.bs-popover-right>.arrow{left:calc(-.5rem - 1px);width:.5rem;height:1rem;margin:.3rem 0}.bs-popover-auto[x-placement^=right]>.arrow::before,.bs-popover-right>.arrow::before{left:0;border-width:.5rem .5rem .5rem 0;border-right-color:rgba(0,0,0,.25)}.bs-popover-auto[x-placement^=right]>.arrow::after,.bs-popover-right>.arrow::after{left:1px;border-width:.5rem .5rem .5rem 0;border-right-color:#fff}.bs-popover-auto[x-placement^=bottom],.bs-popover-bottom{margin-top:.5rem}.bs-popover-auto[x-placement^=bottom]>.arrow,.bs-popover-bottom>.arrow{top:calc(-.5rem - 1px)}.bs-popover-auto[x-placement^=bottom]>.arrow::before,.bs-popover-bottom>.arrow::before{top:0;border-width:0 .5rem .5rem .5rem;border-bottom-color:rgba(0,0,0,.25)}.bs-popover-auto[x-placement^=bottom]>.arrow::after,.bs-popover-bottom>.arrow::after{top:1px;border-width:0 .5rem .5rem .5rem;border-bottom-color:#fff}.bs-popover-auto[x-placement^=bottom] .popover-header::before,.bs-popover-bottom .popover-header::before{position:absolute;top:0;left:50%;display:block;width:1rem;margin-left:-.5rem;content:\"\";border-bottom:1px solid #f7f7f7}.bs-popover-auto[x-placement^=left],.bs-popover-left{margin-right:.5rem}.bs-popover-auto[x-placement^=left]>.arrow,.bs-popover-left>.arrow{right:calc(-.5rem - 1px);width:.5rem;height:1rem;margin:.3rem 0}.bs-popover-auto[x-placement^=left]>.arrow::before,.bs-popover-left>.arrow::before{right:0;border-width:.5rem 0 .5rem .5rem;border-left-color:rgba(0,0,0,.25)}.bs-popover-auto[x-placement^=left]>.arrow::after,.bs-popover-left>.arrow::after{right:1px;border-width:.5rem 0 .5rem .5rem;border-left-color:#fff}.popover-header{padding:.5rem .75rem;margin-bottom:0;font-size:1rem;background-color:#f7f7f7;border-bottom:1px solid #ebebeb;border-top-left-radius:calc(.3rem - 1px);border-top-right-radius:calc(.3rem - 1px)}.popover-header:empty{display:none}.popover-body{padding:.5rem .75rem;color:#212529}.carousel{position:relative}.carousel.pointer-event{-ms-touch-action:pan-y;touch-action:pan-y}.carousel-inner{position:relative;width:100%;overflow:hidden}.carousel-inner::after{display:block;clear:both;content:\"\"}.carousel-item{position:relative;display:none;float:left;width:100%;margin-right:-100%;-webkit-backface-visibility:hidden;backface-visibility:hidden;transition:-webkit-transform .6s ease-in-out;transition:transform .6s ease-in-out;transition:transform .6s ease-in-out,-webkit-transform .6s ease-in-out}@media (prefers-reduced-motion:reduce){.carousel-item{transition:none}}.carousel-item-next,.carousel-item-prev,.carousel-item.active{display:block}.active.carousel-item-right,.carousel-item-next:not(.carousel-item-left){-webkit-transform:translateX(100%);transform:translateX(100%)}.active.carousel-item-left,.carousel-item-prev:not(.carousel-item-right){-webkit-transform:translateX(-100%);transform:translateX(-100%)}.carousel-fade .carousel-item{opacity:0;transition-property:opacity;-webkit-transform:none;transform:none}.carousel-fade .carousel-item-next.carousel-item-left,.carousel-fade .carousel-item-prev.carousel-item-right,.carousel-fade .carousel-item.active{z-index:1;opacity:1}.carousel-fade .active.carousel-item-left,.carousel-fade .active.carousel-item-right{z-index:0;opacity:0;transition:opacity 0s .6s}@media (prefers-reduced-motion:reduce){.carousel-fade .active.carousel-item-left,.carousel-fade .active.carousel-item-right{transition:none}}.carousel-control-next,.carousel-control-prev{position:absolute;top:0;bottom:0;z-index:1;display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;-ms-flex-pack:center;justify-content:center;width:15%;color:#fff;text-align:center;opacity:.5;transition:opacity .15s ease}@media (prefers-reduced-motion:reduce){.carousel-control-next,.carousel-control-prev{transition:none}}.carousel-control-next:focus,.carousel-control-next:hover,.carousel-control-prev:focus,.carousel-control-prev:hover{color:#fff;text-decoration:none;outline:0;opacity:.9}.carousel-control-prev{left:0}.carousel-control-next{right:0}.carousel-control-next-icon,.carousel-control-prev-icon{display:inline-block;width:20px;height:20px;background:no-repeat 50%/100% 100%}.carousel-control-prev-icon{background-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='%23fff' width='8' height='8' viewBox='0 0 8 8'%3e%3cpath d='M5.25 0l-4 4 4 4 1.5-1.5L4.25 4l2.5-2.5L5.25 0z'/%3e%3c/svg%3e\")}.carousel-control-next-icon{background-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='%23fff' width='8' height='8' viewBox='0 0 8 8'%3e%3cpath d='M2.75 0l-1.5 1.5L3.75 4l-2.5 2.5L2.75 8l4-4-4-4z'/%3e%3c/svg%3e\")}.carousel-indicators{position:absolute;right:0;bottom:0;left:0;z-index:15;display:-ms-flexbox;display:flex;-ms-flex-pack:center;justify-content:center;padding-left:0;margin-right:15%;margin-left:15%;list-style:none}.carousel-indicators li{box-sizing:content-box;-ms-flex:0 1 auto;flex:0 1 auto;width:30px;height:3px;margin-right:3px;margin-left:3px;text-indent:-999px;cursor:pointer;background-color:#fff;background-clip:padding-box;border-top:10px solid transparent;border-bottom:10px solid transparent;opacity:.5;transition:opacity .6s ease}@media (prefers-reduced-motion:reduce){.carousel-indicators li{transition:none}}.carousel-indicators .active{opacity:1}.carousel-caption{position:absolute;right:15%;bottom:20px;left:15%;z-index:10;padding-top:20px;padding-bottom:20px;color:#fff;text-align:center}@-webkit-keyframes spinner-border{to{-webkit-transform:rotate(360deg);transform:rotate(360deg)}}@keyframes spinner-border{to{-webkit-transform:rotate(360deg);transform:rotate(360deg)}}.spinner-border{display:inline-block;width:2rem;height:2rem;vertical-align:text-bottom;border:.25em solid currentColor;border-right-color:transparent;border-radius:50%;-webkit-animation:spinner-border .75s linear infinite;animation:spinner-border .75s linear infinite}.spinner-border-sm{width:1rem;height:1rem;border-width:.2em}@-webkit-keyframes spinner-grow{0%{-webkit-transform:scale(0);transform:scale(0)}50%{opacity:1}}@keyframes spinner-grow{0%{-webkit-transform:scale(0);transform:scale(0)}50%{opacity:1}}.spinner-grow{display:inline-block;width:2rem;height:2rem;vertical-align:text-bottom;background-color:currentColor;border-radius:50%;opacity:0;-webkit-animation:spinner-grow .75s linear infinite;animation:spinner-grow .75s linear infinite}.spinner-grow-sm{width:1rem;height:1rem}.align-baseline{vertical-align:baseline!important}.align-top{vertical-align:top!important}.align-middle{vertical-align:middle!important}.align-bottom{vertical-align:bottom!important}.align-text-bottom{vertical-align:text-bottom!important}.align-text-top{vertical-align:text-top!important}.bg-primary{background-color:#007bff!important}a.bg-primary:focus,a.bg-primary:hover,button.bg-primary:focus,button.bg-primary:hover{background-color:#0062cc!important}.bg-secondary{background-color:#6c757d!important}a.bg-secondary:focus,a.bg-secondary:hover,button.bg-secondary:focus,button.bg-secondary:hover{background-color:#545b62!important}.bg-success{background-color:#28a745!important}a.bg-success:focus,a.bg-success:hover,button.bg-success:focus,button.bg-success:hover{background-color:#1e7e34!important}.bg-info{background-color:#17a2b8!important}a.bg-info:focus,a.bg-info:hover,button.bg-info:focus,button.bg-info:hover{background-color:#117a8b!important}.bg-warning{background-color:#ffc107!important}a.bg-warning:focus,a.bg-warning:hover,button.bg-warning:focus,button.bg-warning:hover{background-color:#d39e00!important}.bg-danger{background-color:#dc3545!important}a.bg-danger:focus,a.bg-danger:hover,button.bg-danger:focus,button.bg-danger:hover{background-color:#bd2130!important}.bg-light{background-color:#f8f9fa!important}a.bg-light:focus,a.bg-light:hover,button.bg-light:focus,button.bg-light:hover{background-color:#dae0e5!important}.bg-dark{background-color:#343a40!important}a.bg-dark:focus,a.bg-dark:hover,button.bg-dark:focus,button.bg-dark:hover{background-color:#1d2124!important}.bg-white{background-color:#fff!important}.bg-transparent{background-color:transparent!important}.border{border:1px solid #dee2e6!important}.border-top{border-top:1px solid #dee2e6!important}.border-right{border-right:1px solid #dee2e6!important}.border-bottom{border-bottom:1px solid #dee2e6!important}.border-left{border-left:1px solid #dee2e6!important}.border-0{border:0!important}.border-top-0{border-top:0!important}.border-right-0{border-right:0!important}.border-bottom-0{border-bottom:0!important}.border-left-0{border-left:0!important}.border-primary{border-color:#007bff!important}.border-secondary{border-color:#6c757d!important}.border-success{border-color:#28a745!important}.border-info{border-color:#17a2b8!important}.border-warning{border-color:#ffc107!important}.border-danger{border-color:#dc3545!important}.border-light{border-color:#f8f9fa!important}.border-dark{border-color:#343a40!important}.border-white{border-color:#fff!important}.rounded-sm{border-radius:.2rem!important}.rounded{border-radius:.25rem!important}.rounded-top{border-top-left-radius:.25rem!important;border-top-right-radius:.25rem!important}.rounded-right{border-top-right-radius:.25rem!important;border-bottom-right-radius:.25rem!important}.rounded-bottom{border-bottom-right-radius:.25rem!important;border-bottom-left-radius:.25rem!important}.rounded-left{border-top-left-radius:.25rem!important;border-bottom-left-radius:.25rem!important}.rounded-lg{border-radius:.3rem!important}.rounded-circle{border-radius:50%!important}.rounded-pill{border-radius:50rem!important}.rounded-0{border-radius:0!important}.clearfix::after{display:block;clear:both;content:\"\"}.d-none{display:none!important}.d-inline{display:inline!important}.d-inline-block{display:inline-block!important}.d-block{display:block!important}.d-table{display:table!important}.d-table-row{display:table-row!important}.d-table-cell{display:table-cell!important}.d-flex{display:-ms-flexbox!important;display:flex!important}.d-inline-flex{display:-ms-inline-flexbox!important;display:inline-flex!important}@media (min-width:576px){.d-sm-none{display:none!important}.d-sm-inline{display:inline!important}.d-sm-inline-block{display:inline-block!important}.d-sm-block{display:block!important}.d-sm-table{display:table!important}.d-sm-table-row{display:table-row!important}.d-sm-table-cell{display:table-cell!important}.d-sm-flex{display:-ms-flexbox!important;display:flex!important}.d-sm-inline-flex{display:-ms-inline-flexbox!important;display:inline-flex!important}}@media (min-width:768px){.d-md-none{display:none!important}.d-md-inline{display:inline!important}.d-md-inline-block{display:inline-block!important}.d-md-block{display:block!important}.d-md-table{display:table!important}.d-md-table-row{display:table-row!important}.d-md-table-cell{display:table-cell!important}.d-md-flex{display:-ms-flexbox!important;display:flex!important}.d-md-inline-flex{display:-ms-inline-flexbox!important;display:inline-flex!important}}@media (min-width:992px){.d-lg-none{display:none!important}.d-lg-inline{display:inline!important}.d-lg-inline-block{display:inline-block!important}.d-lg-block{display:block!important}.d-lg-table{display:table!important}.d-lg-table-row{display:table-row!important}.d-lg-table-cell{display:table-cell!important}.d-lg-flex{display:-ms-flexbox!important;display:flex!important}.d-lg-inline-flex{display:-ms-inline-flexbox!important;display:inline-flex!important}}@media (min-width:1200px){.d-xl-none{display:none!important}.d-xl-inline{display:inline!important}.d-xl-inline-block{display:inline-block!important}.d-xl-block{display:block!important}.d-xl-table{display:table!important}.d-xl-table-row{display:table-row!important}.d-xl-table-cell{display:table-cell!important}.d-xl-flex{display:-ms-flexbox!important;display:flex!important}.d-xl-inline-flex{display:-ms-inline-flexbox!important;display:inline-flex!important}}@media print{.d-print-none{display:none!important}.d-print-inline{display:inline!important}.d-print-inline-block{display:inline-block!important}.d-print-block{display:block!important}.d-print-table{display:table!important}.d-print-table-row{display:table-row!important}.d-print-table-cell{display:table-cell!important}.d-print-flex{display:-ms-flexbox!important;display:flex!important}.d-print-inline-flex{display:-ms-inline-flexbox!important;display:inline-flex!important}}.embed-responsive{position:relative;display:block;width:100%;padding:0;overflow:hidden}.embed-responsive::before{display:block;content:\"\"}.embed-responsive .embed-responsive-item,.embed-responsive embed,.embed-responsive iframe,.embed-responsive object,.embed-responsive video{position:absolute;top:0;bottom:0;left:0;width:100%;height:100%;border:0}.embed-responsive-21by9::before{padding-top:42.857143%}.embed-responsive-16by9::before{padding-top:56.25%}.embed-responsive-4by3::before{padding-top:75%}.embed-responsive-1by1::before{padding-top:100%}.flex-row{-ms-flex-direction:row!important;flex-direction:row!important}.flex-column{-ms-flex-direction:column!important;flex-direction:column!important}.flex-row-reverse{-ms-flex-direction:row-reverse!important;flex-direction:row-reverse!important}.flex-column-reverse{-ms-flex-direction:column-reverse!important;flex-direction:column-reverse!important}.flex-wrap{-ms-flex-wrap:wrap!important;flex-wrap:wrap!important}.flex-nowrap{-ms-flex-wrap:nowrap!important;flex-wrap:nowrap!important}.flex-wrap-reverse{-ms-flex-wrap:wrap-reverse!important;flex-wrap:wrap-reverse!important}.flex-fill{-ms-flex:1 1 auto!important;flex:1 1 auto!important}.flex-grow-0{-ms-flex-positive:0!important;flex-grow:0!important}.flex-grow-1{-ms-flex-positive:1!important;flex-grow:1!important}.flex-shrink-0{-ms-flex-negative:0!important;flex-shrink:0!important}.flex-shrink-1{-ms-flex-negative:1!important;flex-shrink:1!important}.justify-content-start{-ms-flex-pack:start!important;justify-content:flex-start!important}.justify-content-end{-ms-flex-pack:end!important;justify-content:flex-end!important}.justify-content-center{-ms-flex-pack:center!important;justify-content:center!important}.justify-content-between{-ms-flex-pack:justify!important;justify-content:space-between!important}.justify-content-around{-ms-flex-pack:distribute!important;justify-content:space-around!important}.align-items-start{-ms-flex-align:start!important;align-items:flex-start!important}.align-items-end{-ms-flex-align:end!important;align-items:flex-end!important}.align-items-center{-ms-flex-align:center!important;align-items:center!important}.align-items-baseline{-ms-flex-align:baseline!important;align-items:baseline!important}.align-items-stretch{-ms-flex-align:stretch!important;align-items:stretch!important}.align-content-start{-ms-flex-line-pack:start!important;align-content:flex-start!important}.align-content-end{-ms-flex-line-pack:end!important;align-content:flex-end!important}.align-content-center{-ms-flex-line-pack:center!important;align-content:center!important}.align-content-between{-ms-flex-line-pack:justify!important;align-content:space-between!important}.align-content-around{-ms-flex-line-pack:distribute!important;align-content:space-around!important}.align-content-stretch{-ms-flex-line-pack:stretch!important;align-content:stretch!important}.align-self-auto{-ms-flex-item-align:auto!important;align-self:auto!important}.align-self-start{-ms-flex-item-align:start!important;align-self:flex-start!important}.align-self-end{-ms-flex-item-align:end!important;align-self:flex-end!important}.align-self-center{-ms-flex-item-align:center!important;align-self:center!important}.align-self-baseline{-ms-flex-item-align:baseline!important;align-self:baseline!important}.align-self-stretch{-ms-flex-item-align:stretch!important;align-self:stretch!important}@media (min-width:576px){.flex-sm-row{-ms-flex-direction:row!important;flex-direction:row!important}.flex-sm-column{-ms-flex-direction:column!important;flex-direction:column!important}.flex-sm-row-reverse{-ms-flex-direction:row-reverse!important;flex-direction:row-reverse!important}.flex-sm-column-reverse{-ms-flex-direction:column-reverse!important;flex-direction:column-reverse!important}.flex-sm-wrap{-ms-flex-wrap:wrap!important;flex-wrap:wrap!important}.flex-sm-nowrap{-ms-flex-wrap:nowrap!important;flex-wrap:nowrap!important}.flex-sm-wrap-reverse{-ms-flex-wrap:wrap-reverse!important;flex-wrap:wrap-reverse!important}.flex-sm-fill{-ms-flex:1 1 auto!important;flex:1 1 auto!important}.flex-sm-grow-0{-ms-flex-positive:0!important;flex-grow:0!important}.flex-sm-grow-1{-ms-flex-positive:1!important;flex-grow:1!important}.flex-sm-shrink-0{-ms-flex-negative:0!important;flex-shrink:0!important}.flex-sm-shrink-1{-ms-flex-negative:1!important;flex-shrink:1!important}.justify-content-sm-start{-ms-flex-pack:start!important;justify-content:flex-start!important}.justify-content-sm-end{-ms-flex-pack:end!important;justify-content:flex-end!important}.justify-content-sm-center{-ms-flex-pack:center!important;justify-content:center!important}.justify-content-sm-between{-ms-flex-pack:justify!important;justify-content:space-between!important}.justify-content-sm-around{-ms-flex-pack:distribute!important;justify-content:space-around!important}.align-items-sm-start{-ms-flex-align:start!important;align-items:flex-start!important}.align-items-sm-end{-ms-flex-align:end!important;align-items:flex-end!important}.align-items-sm-center{-ms-flex-align:center!important;align-items:center!important}.align-items-sm-baseline{-ms-flex-align:baseline!important;align-items:baseline!important}.align-items-sm-stretch{-ms-flex-align:stretch!important;align-items:stretch!important}.align-content-sm-start{-ms-flex-line-pack:start!important;align-content:flex-start!important}.align-content-sm-end{-ms-flex-line-pack:end!important;align-content:flex-end!important}.align-content-sm-center{-ms-flex-line-pack:center!important;align-content:center!important}.align-content-sm-between{-ms-flex-line-pack:justify!important;align-content:space-between!important}.align-content-sm-around{-ms-flex-line-pack:distribute!important;align-content:space-around!important}.align-content-sm-stretch{-ms-flex-line-pack:stretch!important;align-content:stretch!important}.align-self-sm-auto{-ms-flex-item-align:auto!important;align-self:auto!important}.align-self-sm-start{-ms-flex-item-align:start!important;align-self:flex-start!important}.align-self-sm-end{-ms-flex-item-align:end!important;align-self:flex-end!important}.align-self-sm-center{-ms-flex-item-align:center!important;align-self:center!important}.align-self-sm-baseline{-ms-flex-item-align:baseline!important;align-self:baseline!important}.align-self-sm-stretch{-ms-flex-item-align:stretch!important;align-self:stretch!important}}@media (min-width:768px){.flex-md-row{-ms-flex-direction:row!important;flex-direction:row!important}.flex-md-column{-ms-flex-direction:column!important;flex-direction:column!important}.flex-md-row-reverse{-ms-flex-direction:row-reverse!important;flex-direction:row-reverse!important}.flex-md-column-reverse{-ms-flex-direction:column-reverse!important;flex-direction:column-reverse!important}.flex-md-wrap{-ms-flex-wrap:wrap!important;flex-wrap:wrap!important}.flex-md-nowrap{-ms-flex-wrap:nowrap!important;flex-wrap:nowrap!important}.flex-md-wrap-reverse{-ms-flex-wrap:wrap-reverse!important;flex-wrap:wrap-reverse!important}.flex-md-fill{-ms-flex:1 1 auto!important;flex:1 1 auto!important}.flex-md-grow-0{-ms-flex-positive:0!important;flex-grow:0!important}.flex-md-grow-1{-ms-flex-positive:1!important;flex-grow:1!important}.flex-md-shrink-0{-ms-flex-negative:0!important;flex-shrink:0!important}.flex-md-shrink-1{-ms-flex-negative:1!important;flex-shrink:1!important}.justify-content-md-start{-ms-flex-pack:start!important;justify-content:flex-start!important}.justify-content-md-end{-ms-flex-pack:end!important;justify-content:flex-end!important}.justify-content-md-center{-ms-flex-pack:center!important;justify-content:center!important}.justify-content-md-between{-ms-flex-pack:justify!important;justify-content:space-between!important}.justify-content-md-around{-ms-flex-pack:distribute!important;justify-content:space-around!important}.align-items-md-start{-ms-flex-align:start!important;align-items:flex-start!important}.align-items-md-end{-ms-flex-align:end!important;align-items:flex-end!important}.align-items-md-center{-ms-flex-align:center!important;align-items:center!important}.align-items-md-baseline{-ms-flex-align:baseline!important;align-items:baseline!important}.align-items-md-stretch{-ms-flex-align:stretch!important;align-items:stretch!important}.align-content-md-start{-ms-flex-line-pack:start!important;align-content:flex-start!important}.align-content-md-end{-ms-flex-line-pack:end!important;align-content:flex-end!important}.align-content-md-center{-ms-flex-line-pack:center!important;align-content:center!important}.align-content-md-between{-ms-flex-line-pack:justify!important;align-content:space-between!important}.align-content-md-around{-ms-flex-line-pack:distribute!important;align-content:space-around!important}.align-content-md-stretch{-ms-flex-line-pack:stretch!important;align-content:stretch!important}.align-self-md-auto{-ms-flex-item-align:auto!important;align-self:auto!important}.align-self-md-start{-ms-flex-item-align:start!important;align-self:flex-start!important}.align-self-md-end{-ms-flex-item-align:end!important;align-self:flex-end!important}.align-self-md-center{-ms-flex-item-align:center!important;align-self:center!important}.align-self-md-baseline{-ms-flex-item-align:baseline!important;align-self:baseline!important}.align-self-md-stretch{-ms-flex-item-align:stretch!important;align-self:stretch!important}}@media (min-width:992px){.flex-lg-row{-ms-flex-direction:row!important;flex-direction:row!important}.flex-lg-column{-ms-flex-direction:column!important;flex-direction:column!important}.flex-lg-row-reverse{-ms-flex-direction:row-reverse!important;flex-direction:row-reverse!important}.flex-lg-column-reverse{-ms-flex-direction:column-reverse!important;flex-direction:column-reverse!important}.flex-lg-wrap{-ms-flex-wrap:wrap!important;flex-wrap:wrap!important}.flex-lg-nowrap{-ms-flex-wrap:nowrap!important;flex-wrap:nowrap!important}.flex-lg-wrap-reverse{-ms-flex-wrap:wrap-reverse!important;flex-wrap:wrap-reverse!important}.flex-lg-fill{-ms-flex:1 1 auto!important;flex:1 1 auto!important}.flex-lg-grow-0{-ms-flex-positive:0!important;flex-grow:0!important}.flex-lg-grow-1{-ms-flex-positive:1!important;flex-grow:1!important}.flex-lg-shrink-0{-ms-flex-negative:0!important;flex-shrink:0!important}.flex-lg-shrink-1{-ms-flex-negative:1!important;flex-shrink:1!important}.justify-content-lg-start{-ms-flex-pack:start!important;justify-content:flex-start!important}.justify-content-lg-end{-ms-flex-pack:end!important;justify-content:flex-end!important}.justify-content-lg-center{-ms-flex-pack:center!important;justify-content:center!important}.justify-content-lg-between{-ms-flex-pack:justify!important;justify-content:space-between!important}.justify-content-lg-around{-ms-flex-pack:distribute!important;justify-content:space-around!important}.align-items-lg-start{-ms-flex-align:start!important;align-items:flex-start!important}.align-items-lg-end{-ms-flex-align:end!important;align-items:flex-end!important}.align-items-lg-center{-ms-flex-align:center!important;align-items:center!important}.align-items-lg-baseline{-ms-flex-align:baseline!important;align-items:baseline!important}.align-items-lg-stretch{-ms-flex-align:stretch!important;align-items:stretch!important}.align-content-lg-start{-ms-flex-line-pack:start!important;align-content:flex-start!important}.align-content-lg-end{-ms-flex-line-pack:end!important;align-content:flex-end!important}.align-content-lg-center{-ms-flex-line-pack:center!important;align-content:center!important}.align-content-lg-between{-ms-flex-line-pack:justify!important;align-content:space-between!important}.align-content-lg-around{-ms-flex-line-pack:distribute!important;align-content:space-around!important}.align-content-lg-stretch{-ms-flex-line-pack:stretch!important;align-content:stretch!important}.align-self-lg-auto{-ms-flex-item-align:auto!important;align-self:auto!important}.align-self-lg-start{-ms-flex-item-align:start!important;align-self:flex-start!important}.align-self-lg-end{-ms-flex-item-align:end!important;align-self:flex-end!important}.align-self-lg-center{-ms-flex-item-align:center!important;align-self:center!important}.align-self-lg-baseline{-ms-flex-item-align:baseline!important;align-self:baseline!important}.align-self-lg-stretch{-ms-flex-item-align:stretch!important;align-self:stretch!important}}@media (min-width:1200px){.flex-xl-row{-ms-flex-direction:row!important;flex-direction:row!important}.flex-xl-column{-ms-flex-direction:column!important;flex-direction:column!important}.flex-xl-row-reverse{-ms-flex-direction:row-reverse!important;flex-direction:row-reverse!important}.flex-xl-column-reverse{-ms-flex-direction:column-reverse!important;flex-direction:column-reverse!important}.flex-xl-wrap{-ms-flex-wrap:wrap!important;flex-wrap:wrap!important}.flex-xl-nowrap{-ms-flex-wrap:nowrap!important;flex-wrap:nowrap!important}.flex-xl-wrap-reverse{-ms-flex-wrap:wrap-reverse!important;flex-wrap:wrap-reverse!important}.flex-xl-fill{-ms-flex:1 1 auto!important;flex:1 1 auto!important}.flex-xl-grow-0{-ms-flex-positive:0!important;flex-grow:0!important}.flex-xl-grow-1{-ms-flex-positive:1!important;flex-grow:1!important}.flex-xl-shrink-0{-ms-flex-negative:0!important;flex-shrink:0!important}.flex-xl-shrink-1{-ms-flex-negative:1!important;flex-shrink:1!important}.justify-content-xl-start{-ms-flex-pack:start!important;justify-content:flex-start!important}.justify-content-xl-end{-ms-flex-pack:end!important;justify-content:flex-end!important}.justify-content-xl-center{-ms-flex-pack:center!important;justify-content:center!important}.justify-content-xl-between{-ms-flex-pack:justify!important;justify-content:space-between!important}.justify-content-xl-around{-ms-flex-pack:distribute!important;justify-content:space-around!important}.align-items-xl-start{-ms-flex-align:start!important;align-items:flex-start!important}.align-items-xl-end{-ms-flex-align:end!important;align-items:flex-end!important}.align-items-xl-center{-ms-flex-align:center!important;align-items:center!important}.align-items-xl-baseline{-ms-flex-align:baseline!important;align-items:baseline!important}.align-items-xl-stretch{-ms-flex-align:stretch!important;align-items:stretch!important}.align-content-xl-start{-ms-flex-line-pack:start!important;align-content:flex-start!important}.align-content-xl-end{-ms-flex-line-pack:end!important;align-content:flex-end!important}.align-content-xl-center{-ms-flex-line-pack:center!important;align-content:center!important}.align-content-xl-between{-ms-flex-line-pack:justify!important;align-content:space-between!important}.align-content-xl-around{-ms-flex-line-pack:distribute!important;align-content:space-around!important}.align-content-xl-stretch{-ms-flex-line-pack:stretch!important;align-content:stretch!important}.align-self-xl-auto{-ms-flex-item-align:auto!important;align-self:auto!important}.align-self-xl-start{-ms-flex-item-align:start!important;align-self:flex-start!important}.align-self-xl-end{-ms-flex-item-align:end!important;align-self:flex-end!important}.align-self-xl-center{-ms-flex-item-align:center!important;align-self:center!important}.align-self-xl-baseline{-ms-flex-item-align:baseline!important;align-self:baseline!important}.align-self-xl-stretch{-ms-flex-item-align:stretch!important;align-self:stretch!important}}.float-left{float:left!important}.float-right{float:right!important}.float-none{float:none!important}@media (min-width:576px){.float-sm-left{float:left!important}.float-sm-right{float:right!important}.float-sm-none{float:none!important}}@media (min-width:768px){.float-md-left{float:left!important}.float-md-right{float:right!important}.float-md-none{float:none!important}}@media (min-width:992px){.float-lg-left{float:left!important}.float-lg-right{float:right!important}.float-lg-none{float:none!important}}@media (min-width:1200px){.float-xl-left{float:left!important}.float-xl-right{float:right!important}.float-xl-none{float:none!important}}.overflow-auto{overflow:auto!important}.overflow-hidden{overflow:hidden!important}.position-static{position:static!important}.position-relative{position:relative!important}.position-absolute{position:absolute!important}.position-fixed{position:fixed!important}.position-sticky{position:-webkit-sticky!important;position:sticky!important}.fixed-top{position:fixed;top:0;right:0;left:0;z-index:1030}.fixed-bottom{position:fixed;right:0;bottom:0;left:0;z-index:1030}@supports ((position:-webkit-sticky) or (position:sticky)){.sticky-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}.sr-only-focusable:active,.sr-only-focusable:focus{position:static;width:auto;height:auto;overflow:visible;clip:auto;white-space:normal}.shadow-sm{box-shadow:0 .125rem .25rem rgba(0,0,0,.075)!important}.shadow{box-shadow:0 .5rem 1rem rgba(0,0,0,.15)!important}.shadow-lg{box-shadow:0 1rem 3rem rgba(0,0,0,.175)!important}.shadow-none{box-shadow:none!important}.w-25{width:25%!important}.w-50{width:50%!important}.w-75{width:75%!important}.w-100{width:100%!important}.w-auto{width:auto!important}.h-25{height:25%!important}.h-50{height:50%!important}.h-75{height:75%!important}.h-100{height:100%!important}.h-auto{height:auto!important}.mw-100{max-width:100%!important}.mh-100{max-height:100%!important}.min-vw-100{min-width:100vw!important}.min-vh-100{min-height:100vh!important}.vw-100{width:100vw!important}.vh-100{height:100vh!important}.stretched-link::after{position:absolute;top:0;right:0;bottom:0;left:0;z-index:1;pointer-events:auto;content:\"\";background-color:rgba(0,0,0,0)}.m-0{margin:0!important}.mt-0,.my-0{margin-top:0!important}.mr-0,.mx-0{margin-right:0!important}.mb-0,.my-0{margin-bottom:0!important}.ml-0,.mx-0{margin-left:0!important}.m-1{margin:.25rem!important}.mt-1,.my-1{margin-top:.25rem!important}.mr-1,.mx-1{margin-right:.25rem!important}.mb-1,.my-1{margin-bottom:.25rem!important}.ml-1,.mx-1{margin-left:.25rem!important}.m-2{margin:.5rem!important}.mt-2,.my-2{margin-top:.5rem!important}.mr-2,.mx-2{margin-right:.5rem!important}.mb-2,.my-2{margin-bottom:.5rem!important}.ml-2,.mx-2{margin-left:.5rem!important}.m-3{margin:1rem!important}.mt-3,.my-3{margin-top:1rem!important}.mr-3,.mx-3{margin-right:1rem!important}.mb-3,.my-3{margin-bottom:1rem!important}.ml-3,.mx-3{margin-left:1rem!important}.m-4{margin:1.5rem!important}.mt-4,.my-4{margin-top:1.5rem!important}.mr-4,.mx-4{margin-right:1.5rem!important}.mb-4,.my-4{margin-bottom:1.5rem!important}.ml-4,.mx-4{margin-left:1.5rem!important}.m-5{margin:3rem!important}.mt-5,.my-5{margin-top:3rem!important}.mr-5,.mx-5{margin-right:3rem!important}.mb-5,.my-5{margin-bottom:3rem!important}.ml-5,.mx-5{margin-left:3rem!important}.p-0{padding:0!important}.pt-0,.py-0{padding-top:0!important}.pr-0,.px-0{padding-right:0!important}.pb-0,.py-0{padding-bottom:0!important}.pl-0,.px-0{padding-left:0!important}.p-1{padding:.25rem!important}.pt-1,.py-1{padding-top:.25rem!important}.pr-1,.px-1{padding-right:.25rem!important}.pb-1,.py-1{padding-bottom:.25rem!important}.pl-1,.px-1{padding-left:.25rem!important}.p-2{padding:.5rem!important}.pt-2,.py-2{padding-top:.5rem!important}.pr-2,.px-2{padding-right:.5rem!important}.pb-2,.py-2{padding-bottom:.5rem!important}.pl-2,.px-2{padding-left:.5rem!important}.p-3{padding:1rem!important}.pt-3,.py-3{padding-top:1rem!important}.pr-3,.px-3{padding-right:1rem!important}.pb-3,.py-3{padding-bottom:1rem!important}.pl-3,.px-3{padding-left:1rem!important}.p-4{padding:1.5rem!important}.pt-4,.py-4{padding-top:1.5rem!important}.pr-4,.px-4{padding-right:1.5rem!important}.pb-4,.py-4{padding-bottom:1.5rem!important}.pl-4,.px-4{padding-left:1.5rem!important}.p-5{padding:3rem!important}.pt-5,.py-5{padding-top:3rem!important}.pr-5,.px-5{padding-right:3rem!important}.pb-5,.py-5{padding-bottom:3rem!important}.pl-5,.px-5{padding-left:3rem!important}.m-n1{margin:-.25rem!important}.mt-n1,.my-n1{margin-top:-.25rem!important}.mr-n1,.mx-n1{margin-right:-.25rem!important}.mb-n1,.my-n1{margin-bottom:-.25rem!important}.ml-n1,.mx-n1{margin-left:-.25rem!important}.m-n2{margin:-.5rem!important}.mt-n2,.my-n2{margin-top:-.5rem!important}.mr-n2,.mx-n2{margin-right:-.5rem!important}.mb-n2,.my-n2{margin-bottom:-.5rem!important}.ml-n2,.mx-n2{margin-left:-.5rem!important}.m-n3{margin:-1rem!important}.mt-n3,.my-n3{margin-top:-1rem!important}.mr-n3,.mx-n3{margin-right:-1rem!important}.mb-n3,.my-n3{margin-bottom:-1rem!important}.ml-n3,.mx-n3{margin-left:-1rem!important}.m-n4{margin:-1.5rem!important}.mt-n4,.my-n4{margin-top:-1.5rem!important}.mr-n4,.mx-n4{margin-right:-1.5rem!important}.mb-n4,.my-n4{margin-bottom:-1.5rem!important}.ml-n4,.mx-n4{margin-left:-1.5rem!important}.m-n5{margin:-3rem!important}.mt-n5,.my-n5{margin-top:-3rem!important}.mr-n5,.mx-n5{margin-right:-3rem!important}.mb-n5,.my-n5{margin-bottom:-3rem!important}.ml-n5,.mx-n5{margin-left:-3rem!important}.m-auto{margin:auto!important}.mt-auto,.my-auto{margin-top:auto!important}.mr-auto,.mx-auto{margin-right:auto!important}.mb-auto,.my-auto{margin-bottom:auto!important}.ml-auto,.mx-auto{margin-left:auto!important}@media (min-width:576px){.m-sm-0{margin:0!important}.mt-sm-0,.my-sm-0{margin-top:0!important}.mr-sm-0,.mx-sm-0{margin-right:0!important}.mb-sm-0,.my-sm-0{margin-bottom:0!important}.ml-sm-0,.mx-sm-0{margin-left:0!important}.m-sm-1{margin:.25rem!important}.mt-sm-1,.my-sm-1{margin-top:.25rem!important}.mr-sm-1,.mx-sm-1{margin-right:.25rem!important}.mb-sm-1,.my-sm-1{margin-bottom:.25rem!important}.ml-sm-1,.mx-sm-1{margin-left:.25rem!important}.m-sm-2{margin:.5rem!important}.mt-sm-2,.my-sm-2{margin-top:.5rem!important}.mr-sm-2,.mx-sm-2{margin-right:.5rem!important}.mb-sm-2,.my-sm-2{margin-bottom:.5rem!important}.ml-sm-2,.mx-sm-2{margin-left:.5rem!important}.m-sm-3{margin:1rem!important}.mt-sm-3,.my-sm-3{margin-top:1rem!important}.mr-sm-3,.mx-sm-3{margin-right:1rem!important}.mb-sm-3,.my-sm-3{margin-bottom:1rem!important}.ml-sm-3,.mx-sm-3{margin-left:1rem!important}.m-sm-4{margin:1.5rem!important}.mt-sm-4,.my-sm-4{margin-top:1.5rem!important}.mr-sm-4,.mx-sm-4{margin-right:1.5rem!important}.mb-sm-4,.my-sm-4{margin-bottom:1.5rem!important}.ml-sm-4,.mx-sm-4{margin-left:1.5rem!important}.m-sm-5{margin:3rem!important}.mt-sm-5,.my-sm-5{margin-top:3rem!important}.mr-sm-5,.mx-sm-5{margin-right:3rem!important}.mb-sm-5,.my-sm-5{margin-bottom:3rem!important}.ml-sm-5,.mx-sm-5{margin-left:3rem!important}.p-sm-0{padding:0!important}.pt-sm-0,.py-sm-0{padding-top:0!important}.pr-sm-0,.px-sm-0{padding-right:0!important}.pb-sm-0,.py-sm-0{padding-bottom:0!important}.pl-sm-0,.px-sm-0{padding-left:0!important}.p-sm-1{padding:.25rem!important}.pt-sm-1,.py-sm-1{padding-top:.25rem!important}.pr-sm-1,.px-sm-1{padding-right:.25rem!important}.pb-sm-1,.py-sm-1{padding-bottom:.25rem!important}.pl-sm-1,.px-sm-1{padding-left:.25rem!important}.p-sm-2{padding:.5rem!important}.pt-sm-2,.py-sm-2{padding-top:.5rem!important}.pr-sm-2,.px-sm-2{padding-right:.5rem!important}.pb-sm-2,.py-sm-2{padding-bottom:.5rem!important}.pl-sm-2,.px-sm-2{padding-left:.5rem!important}.p-sm-3{padding:1rem!important}.pt-sm-3,.py-sm-3{padding-top:1rem!important}.pr-sm-3,.px-sm-3{padding-right:1rem!important}.pb-sm-3,.py-sm-3{padding-bottom:1rem!important}.pl-sm-3,.px-sm-3{padding-left:1rem!important}.p-sm-4{padding:1.5rem!important}.pt-sm-4,.py-sm-4{padding-top:1.5rem!important}.pr-sm-4,.px-sm-4{padding-right:1.5rem!important}.pb-sm-4,.py-sm-4{padding-bottom:1.5rem!important}.pl-sm-4,.px-sm-4{padding-left:1.5rem!important}.p-sm-5{padding:3rem!important}.pt-sm-5,.py-sm-5{padding-top:3rem!important}.pr-sm-5,.px-sm-5{padding-right:3rem!important}.pb-sm-5,.py-sm-5{padding-bottom:3rem!important}.pl-sm-5,.px-sm-5{padding-left:3rem!important}.m-sm-n1{margin:-.25rem!important}.mt-sm-n1,.my-sm-n1{margin-top:-.25rem!important}.mr-sm-n1,.mx-sm-n1{margin-right:-.25rem!important}.mb-sm-n1,.my-sm-n1{margin-bottom:-.25rem!important}.ml-sm-n1,.mx-sm-n1{margin-left:-.25rem!important}.m-sm-n2{margin:-.5rem!important}.mt-sm-n2,.my-sm-n2{margin-top:-.5rem!important}.mr-sm-n2,.mx-sm-n2{margin-right:-.5rem!important}.mb-sm-n2,.my-sm-n2{margin-bottom:-.5rem!important}.ml-sm-n2,.mx-sm-n2{margin-left:-.5rem!important}.m-sm-n3{margin:-1rem!important}.mt-sm-n3,.my-sm-n3{margin-top:-1rem!important}.mr-sm-n3,.mx-sm-n3{margin-right:-1rem!important}.mb-sm-n3,.my-sm-n3{margin-bottom:-1rem!important}.ml-sm-n3,.mx-sm-n3{margin-left:-1rem!important}.m-sm-n4{margin:-1.5rem!important}.mt-sm-n4,.my-sm-n4{margin-top:-1.5rem!important}.mr-sm-n4,.mx-sm-n4{margin-right:-1.5rem!important}.mb-sm-n4,.my-sm-n4{margin-bottom:-1.5rem!important}.ml-sm-n4,.mx-sm-n4{margin-left:-1.5rem!important}.m-sm-n5{margin:-3rem!important}.mt-sm-n5,.my-sm-n5{margin-top:-3rem!important}.mr-sm-n5,.mx-sm-n5{margin-right:-3rem!important}.mb-sm-n5,.my-sm-n5{margin-bottom:-3rem!important}.ml-sm-n5,.mx-sm-n5{margin-left:-3rem!important}.m-sm-auto{margin:auto!important}.mt-sm-auto,.my-sm-auto{margin-top:auto!important}.mr-sm-auto,.mx-sm-auto{margin-right:auto!important}.mb-sm-auto,.my-sm-auto{margin-bottom:auto!important}.ml-sm-auto,.mx-sm-auto{margin-left:auto!important}}@media (min-width:768px){.m-md-0{margin:0!important}.mt-md-0,.my-md-0{margin-top:0!important}.mr-md-0,.mx-md-0{margin-right:0!important}.mb-md-0,.my-md-0{margin-bottom:0!important}.ml-md-0,.mx-md-0{margin-left:0!important}.m-md-1{margin:.25rem!important}.mt-md-1,.my-md-1{margin-top:.25rem!important}.mr-md-1,.mx-md-1{margin-right:.25rem!important}.mb-md-1,.my-md-1{margin-bottom:.25rem!important}.ml-md-1,.mx-md-1{margin-left:.25rem!important}.m-md-2{margin:.5rem!important}.mt-md-2,.my-md-2{margin-top:.5rem!important}.mr-md-2,.mx-md-2{margin-right:.5rem!important}.mb-md-2,.my-md-2{margin-bottom:.5rem!important}.ml-md-2,.mx-md-2{margin-left:.5rem!important}.m-md-3{margin:1rem!important}.mt-md-3,.my-md-3{margin-top:1rem!important}.mr-md-3,.mx-md-3{margin-right:1rem!important}.mb-md-3,.my-md-3{margin-bottom:1rem!important}.ml-md-3,.mx-md-3{margin-left:1rem!important}.m-md-4{margin:1.5rem!important}.mt-md-4,.my-md-4{margin-top:1.5rem!important}.mr-md-4,.mx-md-4{margin-right:1.5rem!important}.mb-md-4,.my-md-4{margin-bottom:1.5rem!important}.ml-md-4,.mx-md-4{margin-left:1.5rem!important}.m-md-5{margin:3rem!important}.mt-md-5,.my-md-5{margin-top:3rem!important}.mr-md-5,.mx-md-5{margin-right:3rem!important}.mb-md-5,.my-md-5{margin-bottom:3rem!important}.ml-md-5,.mx-md-5{margin-left:3rem!important}.p-md-0{padding:0!important}.pt-md-0,.py-md-0{padding-top:0!important}.pr-md-0,.px-md-0{padding-right:0!important}.pb-md-0,.py-md-0{padding-bottom:0!important}.pl-md-0,.px-md-0{padding-left:0!important}.p-md-1{padding:.25rem!important}.pt-md-1,.py-md-1{padding-top:.25rem!important}.pr-md-1,.px-md-1{padding-right:.25rem!important}.pb-md-1,.py-md-1{padding-bottom:.25rem!important}.pl-md-1,.px-md-1{padding-left:.25rem!important}.p-md-2{padding:.5rem!important}.pt-md-2,.py-md-2{padding-top:.5rem!important}.pr-md-2,.px-md-2{padding-right:.5rem!important}.pb-md-2,.py-md-2{padding-bottom:.5rem!important}.pl-md-2,.px-md-2{padding-left:.5rem!important}.p-md-3{padding:1rem!important}.pt-md-3,.py-md-3{padding-top:1rem!important}.pr-md-3,.px-md-3{padding-right:1rem!important}.pb-md-3,.py-md-3{padding-bottom:1rem!important}.pl-md-3,.px-md-3{padding-left:1rem!important}.p-md-4{padding:1.5rem!important}.pt-md-4,.py-md-4{padding-top:1.5rem!important}.pr-md-4,.px-md-4{padding-right:1.5rem!important}.pb-md-4,.py-md-4{padding-bottom:1.5rem!important}.pl-md-4,.px-md-4{padding-left:1.5rem!important}.p-md-5{padding:3rem!important}.pt-md-5,.py-md-5{padding-top:3rem!important}.pr-md-5,.px-md-5{padding-right:3rem!important}.pb-md-5,.py-md-5{padding-bottom:3rem!important}.pl-md-5,.px-md-5{padding-left:3rem!important}.m-md-n1{margin:-.25rem!important}.mt-md-n1,.my-md-n1{margin-top:-.25rem!important}.mr-md-n1,.mx-md-n1{margin-right:-.25rem!important}.mb-md-n1,.my-md-n1{margin-bottom:-.25rem!important}.ml-md-n1,.mx-md-n1{margin-left:-.25rem!important}.m-md-n2{margin:-.5rem!important}.mt-md-n2,.my-md-n2{margin-top:-.5rem!important}.mr-md-n2,.mx-md-n2{margin-right:-.5rem!important}.mb-md-n2,.my-md-n2{margin-bottom:-.5rem!important}.ml-md-n2,.mx-md-n2{margin-left:-.5rem!important}.m-md-n3{margin:-1rem!important}.mt-md-n3,.my-md-n3{margin-top:-1rem!important}.mr-md-n3,.mx-md-n3{margin-right:-1rem!important}.mb-md-n3,.my-md-n3{margin-bottom:-1rem!important}.ml-md-n3,.mx-md-n3{margin-left:-1rem!important}.m-md-n4{margin:-1.5rem!important}.mt-md-n4,.my-md-n4{margin-top:-1.5rem!important}.mr-md-n4,.mx-md-n4{margin-right:-1.5rem!important}.mb-md-n4,.my-md-n4{margin-bottom:-1.5rem!important}.ml-md-n4,.mx-md-n4{margin-left:-1.5rem!important}.m-md-n5{margin:-3rem!important}.mt-md-n5,.my-md-n5{margin-top:-3rem!important}.mr-md-n5,.mx-md-n5{margin-right:-3rem!important}.mb-md-n5,.my-md-n5{margin-bottom:-3rem!important}.ml-md-n5,.mx-md-n5{margin-left:-3rem!important}.m-md-auto{margin:auto!important}.mt-md-auto,.my-md-auto{margin-top:auto!important}.mr-md-auto,.mx-md-auto{margin-right:auto!important}.mb-md-auto,.my-md-auto{margin-bottom:auto!important}.ml-md-auto,.mx-md-auto{margin-left:auto!important}}@media (min-width:992px){.m-lg-0{margin:0!important}.mt-lg-0,.my-lg-0{margin-top:0!important}.mr-lg-0,.mx-lg-0{margin-right:0!important}.mb-lg-0,.my-lg-0{margin-bottom:0!important}.ml-lg-0,.mx-lg-0{margin-left:0!important}.m-lg-1{margin:.25rem!important}.mt-lg-1,.my-lg-1{margin-top:.25rem!important}.mr-lg-1,.mx-lg-1{margin-right:.25rem!important}.mb-lg-1,.my-lg-1{margin-bottom:.25rem!important}.ml-lg-1,.mx-lg-1{margin-left:.25rem!important}.m-lg-2{margin:.5rem!important}.mt-lg-2,.my-lg-2{margin-top:.5rem!important}.mr-lg-2,.mx-lg-2{margin-right:.5rem!important}.mb-lg-2,.my-lg-2{margin-bottom:.5rem!important}.ml-lg-2,.mx-lg-2{margin-left:.5rem!important}.m-lg-3{margin:1rem!important}.mt-lg-3,.my-lg-3{margin-top:1rem!important}.mr-lg-3,.mx-lg-3{margin-right:1rem!important}.mb-lg-3,.my-lg-3{margin-bottom:1rem!important}.ml-lg-3,.mx-lg-3{margin-left:1rem!important}.m-lg-4{margin:1.5rem!important}.mt-lg-4,.my-lg-4{margin-top:1.5rem!important}.mr-lg-4,.mx-lg-4{margin-right:1.5rem!important}.mb-lg-4,.my-lg-4{margin-bottom:1.5rem!important}.ml-lg-4,.mx-lg-4{margin-left:1.5rem!important}.m-lg-5{margin:3rem!important}.mt-lg-5,.my-lg-5{margin-top:3rem!important}.mr-lg-5,.mx-lg-5{margin-right:3rem!important}.mb-lg-5,.my-lg-5{margin-bottom:3rem!important}.ml-lg-5,.mx-lg-5{margin-left:3rem!important}.p-lg-0{padding:0!important}.pt-lg-0,.py-lg-0{padding-top:0!important}.pr-lg-0,.px-lg-0{padding-right:0!important}.pb-lg-0,.py-lg-0{padding-bottom:0!important}.pl-lg-0,.px-lg-0{padding-left:0!important}.p-lg-1{padding:.25rem!important}.pt-lg-1,.py-lg-1{padding-top:.25rem!important}.pr-lg-1,.px-lg-1{padding-right:.25rem!important}.pb-lg-1,.py-lg-1{padding-bottom:.25rem!important}.pl-lg-1,.px-lg-1{padding-left:.25rem!important}.p-lg-2{padding:.5rem!important}.pt-lg-2,.py-lg-2{padding-top:.5rem!important}.pr-lg-2,.px-lg-2{padding-right:.5rem!important}.pb-lg-2,.py-lg-2{padding-bottom:.5rem!important}.pl-lg-2,.px-lg-2{padding-left:.5rem!important}.p-lg-3{padding:1rem!important}.pt-lg-3,.py-lg-3{padding-top:1rem!important}.pr-lg-3,.px-lg-3{padding-right:1rem!important}.pb-lg-3,.py-lg-3{padding-bottom:1rem!important}.pl-lg-3,.px-lg-3{padding-left:1rem!important}.p-lg-4{padding:1.5rem!important}.pt-lg-4,.py-lg-4{padding-top:1.5rem!important}.pr-lg-4,.px-lg-4{padding-right:1.5rem!important}.pb-lg-4,.py-lg-4{padding-bottom:1.5rem!important}.pl-lg-4,.px-lg-4{padding-left:1.5rem!important}.p-lg-5{padding:3rem!important}.pt-lg-5,.py-lg-5{padding-top:3rem!important}.pr-lg-5,.px-lg-5{padding-right:3rem!important}.pb-lg-5,.py-lg-5{padding-bottom:3rem!important}.pl-lg-5,.px-lg-5{padding-left:3rem!important}.m-lg-n1{margin:-.25rem!important}.mt-lg-n1,.my-lg-n1{margin-top:-.25rem!important}.mr-lg-n1,.mx-lg-n1{margin-right:-.25rem!important}.mb-lg-n1,.my-lg-n1{margin-bottom:-.25rem!important}.ml-lg-n1,.mx-lg-n1{margin-left:-.25rem!important}.m-lg-n2{margin:-.5rem!important}.mt-lg-n2,.my-lg-n2{margin-top:-.5rem!important}.mr-lg-n2,.mx-lg-n2{margin-right:-.5rem!important}.mb-lg-n2,.my-lg-n2{margin-bottom:-.5rem!important}.ml-lg-n2,.mx-lg-n2{margin-left:-.5rem!important}.m-lg-n3{margin:-1rem!important}.mt-lg-n3,.my-lg-n3{margin-top:-1rem!important}.mr-lg-n3,.mx-lg-n3{margin-right:-1rem!important}.mb-lg-n3,.my-lg-n3{margin-bottom:-1rem!important}.ml-lg-n3,.mx-lg-n3{margin-left:-1rem!important}.m-lg-n4{margin:-1.5rem!important}.mt-lg-n4,.my-lg-n4{margin-top:-1.5rem!important}.mr-lg-n4,.mx-lg-n4{margin-right:-1.5rem!important}.mb-lg-n4,.my-lg-n4{margin-bottom:-1.5rem!important}.ml-lg-n4,.mx-lg-n4{margin-left:-1.5rem!important}.m-lg-n5{margin:-3rem!important}.mt-lg-n5,.my-lg-n5{margin-top:-3rem!important}.mr-lg-n5,.mx-lg-n5{margin-right:-3rem!important}.mb-lg-n5,.my-lg-n5{margin-bottom:-3rem!important}.ml-lg-n5,.mx-lg-n5{margin-left:-3rem!important}.m-lg-auto{margin:auto!important}.mt-lg-auto,.my-lg-auto{margin-top:auto!important}.mr-lg-auto,.mx-lg-auto{margin-right:auto!important}.mb-lg-auto,.my-lg-auto{margin-bottom:auto!important}.ml-lg-auto,.mx-lg-auto{margin-left:auto!important}}@media (min-width:1200px){.m-xl-0{margin:0!important}.mt-xl-0,.my-xl-0{margin-top:0!important}.mr-xl-0,.mx-xl-0{margin-right:0!important}.mb-xl-0,.my-xl-0{margin-bottom:0!important}.ml-xl-0,.mx-xl-0{margin-left:0!important}.m-xl-1{margin:.25rem!important}.mt-xl-1,.my-xl-1{margin-top:.25rem!important}.mr-xl-1,.mx-xl-1{margin-right:.25rem!important}.mb-xl-1,.my-xl-1{margin-bottom:.25rem!important}.ml-xl-1,.mx-xl-1{margin-left:.25rem!important}.m-xl-2{margin:.5rem!important}.mt-xl-2,.my-xl-2{margin-top:.5rem!important}.mr-xl-2,.mx-xl-2{margin-right:.5rem!important}.mb-xl-2,.my-xl-2{margin-bottom:.5rem!important}.ml-xl-2,.mx-xl-2{margin-left:.5rem!important}.m-xl-3{margin:1rem!important}.mt-xl-3,.my-xl-3{margin-top:1rem!important}.mr-xl-3,.mx-xl-3{margin-right:1rem!important}.mb-xl-3,.my-xl-3{margin-bottom:1rem!important}.ml-xl-3,.mx-xl-3{margin-left:1rem!important}.m-xl-4{margin:1.5rem!important}.mt-xl-4,.my-xl-4{margin-top:1.5rem!important}.mr-xl-4,.mx-xl-4{margin-right:1.5rem!important}.mb-xl-4,.my-xl-4{margin-bottom:1.5rem!important}.ml-xl-4,.mx-xl-4{margin-left:1.5rem!important}.m-xl-5{margin:3rem!important}.mt-xl-5,.my-xl-5{margin-top:3rem!important}.mr-xl-5,.mx-xl-5{margin-right:3rem!important}.mb-xl-5,.my-xl-5{margin-bottom:3rem!important}.ml-xl-5,.mx-xl-5{margin-left:3rem!important}.p-xl-0{padding:0!important}.pt-xl-0,.py-xl-0{padding-top:0!important}.pr-xl-0,.px-xl-0{padding-right:0!important}.pb-xl-0,.py-xl-0{padding-bottom:0!important}.pl-xl-0,.px-xl-0{padding-left:0!important}.p-xl-1{padding:.25rem!important}.pt-xl-1,.py-xl-1{padding-top:.25rem!important}.pr-xl-1,.px-xl-1{padding-right:.25rem!important}.pb-xl-1,.py-xl-1{padding-bottom:.25rem!important}.pl-xl-1,.px-xl-1{padding-left:.25rem!important}.p-xl-2{padding:.5rem!important}.pt-xl-2,.py-xl-2{padding-top:.5rem!important}.pr-xl-2,.px-xl-2{padding-right:.5rem!important}.pb-xl-2,.py-xl-2{padding-bottom:.5rem!important}.pl-xl-2,.px-xl-2{padding-left:.5rem!important}.p-xl-3{padding:1rem!important}.pt-xl-3,.py-xl-3{padding-top:1rem!important}.pr-xl-3,.px-xl-3{padding-right:1rem!important}.pb-xl-3,.py-xl-3{padding-bottom:1rem!important}.pl-xl-3,.px-xl-3{padding-left:1rem!important}.p-xl-4{padding:1.5rem!important}.pt-xl-4,.py-xl-4{padding-top:1.5rem!important}.pr-xl-4,.px-xl-4{padding-right:1.5rem!important}.pb-xl-4,.py-xl-4{padding-bottom:1.5rem!important}.pl-xl-4,.px-xl-4{padding-left:1.5rem!important}.p-xl-5{padding:3rem!important}.pt-xl-5,.py-xl-5{padding-top:3rem!important}.pr-xl-5,.px-xl-5{padding-right:3rem!important}.pb-xl-5,.py-xl-5{padding-bottom:3rem!important}.pl-xl-5,.px-xl-5{padding-left:3rem!important}.m-xl-n1{margin:-.25rem!important}.mt-xl-n1,.my-xl-n1{margin-top:-.25rem!important}.mr-xl-n1,.mx-xl-n1{margin-right:-.25rem!important}.mb-xl-n1,.my-xl-n1{margin-bottom:-.25rem!important}.ml-xl-n1,.mx-xl-n1{margin-left:-.25rem!important}.m-xl-n2{margin:-.5rem!important}.mt-xl-n2,.my-xl-n2{margin-top:-.5rem!important}.mr-xl-n2,.mx-xl-n2{margin-right:-.5rem!important}.mb-xl-n2,.my-xl-n2{margin-bottom:-.5rem!important}.ml-xl-n2,.mx-xl-n2{margin-left:-.5rem!important}.m-xl-n3{margin:-1rem!important}.mt-xl-n3,.my-xl-n3{margin-top:-1rem!important}.mr-xl-n3,.mx-xl-n3{margin-right:-1rem!important}.mb-xl-n3,.my-xl-n3{margin-bottom:-1rem!important}.ml-xl-n3,.mx-xl-n3{margin-left:-1rem!important}.m-xl-n4{margin:-1.5rem!important}.mt-xl-n4,.my-xl-n4{margin-top:-1.5rem!important}.mr-xl-n4,.mx-xl-n4{margin-right:-1.5rem!important}.mb-xl-n4,.my-xl-n4{margin-bottom:-1.5rem!important}.ml-xl-n4,.mx-xl-n4{margin-left:-1.5rem!important}.m-xl-n5{margin:-3rem!important}.mt-xl-n5,.my-xl-n5{margin-top:-3rem!important}.mr-xl-n5,.mx-xl-n5{margin-right:-3rem!important}.mb-xl-n5,.my-xl-n5{margin-bottom:-3rem!important}.ml-xl-n5,.mx-xl-n5{margin-left:-3rem!important}.m-xl-auto{margin:auto!important}.mt-xl-auto,.my-xl-auto{margin-top:auto!important}.mr-xl-auto,.mx-xl-auto{margin-right:auto!important}.mb-xl-auto,.my-xl-auto{margin-bottom:auto!important}.ml-xl-auto,.mx-xl-auto{margin-left:auto!important}}.text-monospace{font-family:SFMono-Regular,Menlo,Monaco,Consolas,\"Liberation Mono\",\"Courier New\",monospace!important}.text-justify{text-align:justify!important}.text-wrap{white-space:normal!important}.text-nowrap{white-space:nowrap!important}.text-truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.text-left{text-align:left!important}.text-right{text-align:right!important}.text-center{text-align:center!important}@media (min-width:576px){.text-sm-left{text-align:left!important}.text-sm-right{text-align:right!important}.text-sm-center{text-align:center!important}}@media (min-width:768px){.text-md-left{text-align:left!important}.text-md-right{text-align:right!important}.text-md-center{text-align:center!important}}@media (min-width:992px){.text-lg-left{text-align:left!important}.text-lg-right{text-align:right!important}.text-lg-center{text-align:center!important}}@media (min-width:1200px){.text-xl-left{text-align:left!important}.text-xl-right{text-align:right!important}.text-xl-center{text-align:center!important}}.text-lowercase{text-transform:lowercase!important}.text-uppercase{text-transform:uppercase!important}.text-capitalize{text-transform:capitalize!important}.font-weight-light{font-weight:300!important}.font-weight-lighter{font-weight:lighter!important}.font-weight-normal{font-weight:400!important}.font-weight-bold{font-weight:700!important}.font-weight-bolder{font-weight:bolder!important}.font-italic{font-style:italic!important}.text-white{color:#fff!important}.text-primary{color:#007bff!important}a.text-primary:focus,a.text-primary:hover{color:#0056b3!important}.text-secondary{color:#6c757d!important}a.text-secondary:focus,a.text-secondary:hover{color:#494f54!important}.text-success{color:#28a745!important}a.text-success:focus,a.text-success:hover{color:#19692c!important}.text-info{color:#17a2b8!important}a.text-info:focus,a.text-info:hover{color:#0f6674!important}.text-warning{color:#ffc107!important}a.text-warning:focus,a.text-warning:hover{color:#ba8b00!important}.text-danger{color:#dc3545!important}a.text-danger:focus,a.text-danger:hover{color:#a71d2a!important}.text-light{color:#f8f9fa!important}a.text-light:focus,a.text-light:hover{color:#cbd3da!important}.text-dark{color:#343a40!important}a.text-dark:focus,a.text-dark:hover{color:#121416!important}.text-body{color:#212529!important}.text-muted{color:#6c757d!important}.text-black-50{color:rgba(0,0,0,.5)!important}.text-white-50{color:rgba(255,255,255,.5)!important}.text-hide{font:0/0 a;color:transparent;text-shadow:none;background-color:transparent;border:0}.text-decoration-none{text-decoration:none!important}.text-break{word-break:break-word!important;overflow-wrap:break-word!important}.text-reset{color:inherit!important}.visible{visibility:visible!important}.invisible{visibility:hidden!important}@media print{*,::after,::before{text-shadow:none!important;box-shadow:none!important}a:not(.btn){text-decoration:underline}abbr[title]::after{content:\" (\" attr(title) \")\"}pre{white-space:pre-wrap!important}blockquote,pre{border:1px solid #adb5bd;page-break-inside:avoid}thead{display:table-header-group}img,tr{page-break-inside:avoid}h2,h3,p{orphans:3;widows:3}h2,h3{page-break-after:avoid}@page{size:a3}body{min-width:992px!important}.container{min-width:992px!important}.navbar{display:none}.badge{border:1px solid #000}.table{border-collapse:collapse!important}.table td,.table th{background-color:#fff!important}.table-bordered td,.table-bordered th{border:1px solid #dee2e6!important}.table-dark{color:inherit}.table-dark tbody+tbody,.table-dark td,.table-dark th,.table-dark thead th{border-color:#dee2e6}.table .thead-dark th{color:inherit;border-color:#dee2e6}}\n/*# sourceMappingURL=bootstrap.css.map */"
  },
  {
    "path": "examples/django_example/django_example/static/css/custom.css",
    "content": "/* Alternate the background color of the output rows */\n.list-group-item:nth-child(even) {\n    background-color: #bdf1ac;\n}\n\n.chat-log {\n    max-height:200px;\n    overflow-y:scroll;\n}"
  },
  {
    "path": "examples/django_example/django_example/static/js/bootstrap.js",
    "content": "/*!\n  * Bootstrap v4.4.1 (https://getbootstrap.com/)\n  * Copyright 2011-2019 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors)\n  * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)\n  */\n!function(e,t){\"object\"==typeof exports&&\"undefined\"!=typeof module?t(exports,require(\"jquery\")):\"function\"==typeof define&&define.amd?define([\"exports\",\"jquery\"],t):t((e=e||self).bootstrap={},e.jQuery)}(this,function(e,p){\"use strict\";function i(e,t){for(var n=0;n<t.length;n++){var i=t[n];i.enumerable=i.enumerable||!1,i.configurable=!0,\"value\"in i&&(i.writable=!0),Object.defineProperty(e,i.key,i)}}function s(e,t,n){return t&&i(e.prototype,t),n&&i(e,n),e}function t(t,e){var n=Object.keys(t);if(Object.getOwnPropertySymbols){var i=Object.getOwnPropertySymbols(t);e&&(i=i.filter(function(e){return Object.getOwnPropertyDescriptor(t,e).enumerable})),n.push.apply(n,i)}return n}function l(o){for(var e=1;e<arguments.length;e++){var r=null!=arguments[e]?arguments[e]:{};e%2?t(Object(r),!0).forEach(function(e){var t,n,i;t=o,i=r[n=e],n in t?Object.defineProperty(t,n,{value:i,enumerable:!0,configurable:!0,writable:!0}):t[n]=i}):Object.getOwnPropertyDescriptors?Object.defineProperties(o,Object.getOwnPropertyDescriptors(r)):t(Object(r)).forEach(function(e){Object.defineProperty(o,e,Object.getOwnPropertyDescriptor(r,e))})}return o}p=p&&p.hasOwnProperty(\"default\")?p.default:p;var n=\"transitionend\";function o(e){var t=this,n=!1;return p(this).one(m.TRANSITION_END,function(){n=!0}),setTimeout(function(){n||m.triggerTransitionEnd(t)},e),this}var m={TRANSITION_END:\"bsTransitionEnd\",getUID:function(e){for(;e+=~~(1e6*Math.random()),document.getElementById(e););return e},getSelectorFromElement:function(e){var t=e.getAttribute(\"data-target\");if(!t||\"#\"===t){var n=e.getAttribute(\"href\");t=n&&\"#\"!==n?n.trim():\"\"}try{return document.querySelector(t)?t:null}catch(e){return null}},getTransitionDurationFromElement:function(e){if(!e)return 0;var t=p(e).css(\"transition-duration\"),n=p(e).css(\"transition-delay\"),i=parseFloat(t),o=parseFloat(n);return i||o?(t=t.split(\",\")[0],n=n.split(\",\")[0],1e3*(parseFloat(t)+parseFloat(n))):0},reflow:function(e){return e.offsetHeight},triggerTransitionEnd:function(e){p(e).trigger(n)},supportsTransitionEnd:function(){return Boolean(n)},isElement:function(e){return(e[0]||e).nodeType},typeCheckConfig:function(e,t,n){for(var i in n)if(Object.prototype.hasOwnProperty.call(n,i)){var o=n[i],r=t[i],s=r&&m.isElement(r)?\"element\":(a=r,{}.toString.call(a).match(/\\s([a-z]+)/i)[1].toLowerCase());if(!new RegExp(o).test(s))throw new Error(e.toUpperCase()+': Option \"'+i+'\" provided type \"'+s+'\" but expected type \"'+o+'\".')}var a},findShadowRoot:function(e){if(!document.documentElement.attachShadow)return null;if(\"function\"!=typeof e.getRootNode)return e instanceof ShadowRoot?e:e.parentNode?m.findShadowRoot(e.parentNode):null;var t=e.getRootNode();return t instanceof ShadowRoot?t:null},jQueryDetection:function(){if(\"undefined\"==typeof p)throw new TypeError(\"Bootstrap's JavaScript requires jQuery. jQuery must be included before Bootstrap's JavaScript.\");var e=p.fn.jquery.split(\" \")[0].split(\".\");if(e[0]<2&&e[1]<9||1===e[0]&&9===e[1]&&e[2]<1||4<=e[0])throw new Error(\"Bootstrap's JavaScript requires at least jQuery v1.9.1 but less than v4.0.0\")}};m.jQueryDetection(),p.fn.emulateTransitionEnd=o,p.event.special[m.TRANSITION_END]={bindType:n,delegateType:n,handle:function(e){if(p(e.target).is(this))return e.handleObj.handler.apply(this,arguments)}};var r=\"alert\",a=\"bs.alert\",c=\".\"+a,h=p.fn[r],u={CLOSE:\"close\"+c,CLOSED:\"closed\"+c,CLICK_DATA_API:\"click\"+c+\".data-api\"},f=\"alert\",d=\"fade\",g=\"show\",_=function(){function i(e){this._element=e}var e=i.prototype;return e.close=function(e){var t=this._element;e&&(t=this._getRootElement(e)),this._triggerCloseEvent(t).isDefaultPrevented()||this._removeElement(t)},e.dispose=function(){p.removeData(this._element,a),this._element=null},e._getRootElement=function(e){var t=m.getSelectorFromElement(e),n=!1;return t&&(n=document.querySelector(t)),n=n||p(e).closest(\".\"+f)[0]},e._triggerCloseEvent=function(e){var t=p.Event(u.CLOSE);return p(e).trigger(t),t},e._removeElement=function(t){var n=this;if(p(t).removeClass(g),p(t).hasClass(d)){var e=m.getTransitionDurationFromElement(t);p(t).one(m.TRANSITION_END,function(e){return n._destroyElement(t,e)}).emulateTransitionEnd(e)}else this._destroyElement(t)},e._destroyElement=function(e){p(e).detach().trigger(u.CLOSED).remove()},i._jQueryInterface=function(n){return this.each(function(){var e=p(this),t=e.data(a);t||(t=new i(this),e.data(a,t)),\"close\"===n&&t[n](this)})},i._handleDismiss=function(t){return function(e){e&&e.preventDefault(),t.close(this)}},s(i,null,[{key:\"VERSION\",get:function(){return\"4.4.1\"}}]),i}();p(document).on(u.CLICK_DATA_API,'[data-dismiss=\"alert\"]',_._handleDismiss(new _)),p.fn[r]=_._jQueryInterface,p.fn[r].Constructor=_,p.fn[r].noConflict=function(){return p.fn[r]=h,_._jQueryInterface};var v=\"button\",y=\"bs.button\",E=\".\"+y,b=\".data-api\",w=p.fn[v],T=\"active\",C=\"btn\",S=\"focus\",D='[data-toggle^=\"button\"]',I='[data-toggle=\"buttons\"]',A='[data-toggle=\"button\"]',O='[data-toggle=\"buttons\"] .btn',N='input:not([type=\"hidden\"])',k=\".active\",L=\".btn\",P={CLICK_DATA_API:\"click\"+E+b,FOCUS_BLUR_DATA_API:\"focus\"+E+b+\" blur\"+E+b,LOAD_DATA_API:\"load\"+E+b},x=function(){function n(e){this._element=e}var e=n.prototype;return e.toggle=function(){var e=!0,t=!0,n=p(this._element).closest(I)[0];if(n){var i=this._element.querySelector(N);if(i){if(\"radio\"===i.type)if(i.checked&&this._element.classList.contains(T))e=!1;else{var o=n.querySelector(k);o&&p(o).removeClass(T)}else\"checkbox\"===i.type?\"LABEL\"===this._element.tagName&&i.checked===this._element.classList.contains(T)&&(e=!1):e=!1;e&&(i.checked=!this._element.classList.contains(T),p(i).trigger(\"change\")),i.focus(),t=!1}}this._element.hasAttribute(\"disabled\")||this._element.classList.contains(\"disabled\")||(t&&this._element.setAttribute(\"aria-pressed\",!this._element.classList.contains(T)),e&&p(this._element).toggleClass(T))},e.dispose=function(){p.removeData(this._element,y),this._element=null},n._jQueryInterface=function(t){return this.each(function(){var e=p(this).data(y);e||(e=new n(this),p(this).data(y,e)),\"toggle\"===t&&e[t]()})},s(n,null,[{key:\"VERSION\",get:function(){return\"4.4.1\"}}]),n}();p(document).on(P.CLICK_DATA_API,D,function(e){var t=e.target;if(p(t).hasClass(C)||(t=p(t).closest(L)[0]),!t||t.hasAttribute(\"disabled\")||t.classList.contains(\"disabled\"))e.preventDefault();else{var n=t.querySelector(N);if(n&&(n.hasAttribute(\"disabled\")||n.classList.contains(\"disabled\")))return void e.preventDefault();x._jQueryInterface.call(p(t),\"toggle\")}}).on(P.FOCUS_BLUR_DATA_API,D,function(e){var t=p(e.target).closest(L)[0];p(t).toggleClass(S,/^focus(in)?$/.test(e.type))}),p(window).on(P.LOAD_DATA_API,function(){for(var e=[].slice.call(document.querySelectorAll(O)),t=0,n=e.length;t<n;t++){var i=e[t],o=i.querySelector(N);o.checked||o.hasAttribute(\"checked\")?i.classList.add(T):i.classList.remove(T)}for(var r=0,s=(e=[].slice.call(document.querySelectorAll(A))).length;r<s;r++){var a=e[r];\"true\"===a.getAttribute(\"aria-pressed\")?a.classList.add(T):a.classList.remove(T)}}),p.fn[v]=x._jQueryInterface,p.fn[v].Constructor=x,p.fn[v].noConflict=function(){return p.fn[v]=w,x._jQueryInterface};var j=\"carousel\",H=\"bs.carousel\",R=\".\"+H,F=\".data-api\",M=p.fn[j],W={interval:5e3,keyboard:!0,slide:!1,pause:\"hover\",wrap:!0,touch:!0},U={interval:\"(number|boolean)\",keyboard:\"boolean\",slide:\"(boolean|string)\",pause:\"(string|boolean)\",wrap:\"boolean\",touch:\"boolean\"},B=\"next\",q=\"prev\",K=\"left\",Q=\"right\",V={SLIDE:\"slide\"+R,SLID:\"slid\"+R,KEYDOWN:\"keydown\"+R,MOUSEENTER:\"mouseenter\"+R,MOUSELEAVE:\"mouseleave\"+R,TOUCHSTART:\"touchstart\"+R,TOUCHMOVE:\"touchmove\"+R,TOUCHEND:\"touchend\"+R,POINTERDOWN:\"pointerdown\"+R,POINTERUP:\"pointerup\"+R,DRAG_START:\"dragstart\"+R,LOAD_DATA_API:\"load\"+R+F,CLICK_DATA_API:\"click\"+R+F},Y=\"carousel\",z=\"active\",X=\"slide\",G=\"carousel-item-right\",$=\"carousel-item-left\",J=\"carousel-item-next\",Z=\"carousel-item-prev\",ee=\"pointer-event\",te=\".active\",ne=\".active.carousel-item\",ie=\".carousel-item\",oe=\".carousel-item img\",re=\".carousel-item-next, .carousel-item-prev\",se=\".carousel-indicators\",ae=\"[data-slide], [data-slide-to]\",le='[data-ride=\"carousel\"]',ce={TOUCH:\"touch\",PEN:\"pen\"},he=function(){function r(e,t){this._items=null,this._interval=null,this._activeElement=null,this._isPaused=!1,this._isSliding=!1,this.touchTimeout=null,this.touchStartX=0,this.touchDeltaX=0,this._config=this._getConfig(t),this._element=e,this._indicatorsElement=this._element.querySelector(se),this._touchSupported=\"ontouchstart\"in document.documentElement||0<navigator.maxTouchPoints,this._pointerEvent=Boolean(window.PointerEvent||window.MSPointerEvent),this._addEventListeners()}var e=r.prototype;return e.next=function(){this._isSliding||this._slide(B)},e.nextWhenVisible=function(){!document.hidden&&p(this._element).is(\":visible\")&&\"hidden\"!==p(this._element).css(\"visibility\")&&this.next()},e.prev=function(){this._isSliding||this._slide(q)},e.pause=function(e){e||(this._isPaused=!0),this._element.querySelector(re)&&(m.triggerTransitionEnd(this._element),this.cycle(!0)),clearInterval(this._interval),this._interval=null},e.cycle=function(e){e||(this._isPaused=!1),this._interval&&(clearInterval(this._interval),this._interval=null),this._config.interval&&!this._isPaused&&(this._interval=setInterval((document.visibilityState?this.nextWhenVisible:this.next).bind(this),this._config.interval))},e.to=function(e){var t=this;this._activeElement=this._element.querySelector(ne);var n=this._getItemIndex(this._activeElement);if(!(e>this._items.length-1||e<0))if(this._isSliding)p(this._element).one(V.SLID,function(){return t.to(e)});else{if(n===e)return this.pause(),void this.cycle();var i=n<e?B:q;this._slide(i,this._items[e])}},e.dispose=function(){p(this._element).off(R),p.removeData(this._element,H),this._items=null,this._config=null,this._element=null,this._interval=null,this._isPaused=null,this._isSliding=null,this._activeElement=null,this._indicatorsElement=null},e._getConfig=function(e){return e=l({},W,{},e),m.typeCheckConfig(j,e,U),e},e._handleSwipe=function(){var e=Math.abs(this.touchDeltaX);if(!(e<=40)){var t=e/this.touchDeltaX;(this.touchDeltaX=0)<t&&this.prev(),t<0&&this.next()}},e._addEventListeners=function(){var t=this;this._config.keyboard&&p(this._element).on(V.KEYDOWN,function(e){return t._keydown(e)}),\"hover\"===this._config.pause&&p(this._element).on(V.MOUSEENTER,function(e){return t.pause(e)}).on(V.MOUSELEAVE,function(e){return t.cycle(e)}),this._config.touch&&this._addTouchEventListeners()},e._addTouchEventListeners=function(){var t=this;if(this._touchSupported){var n=function(e){t._pointerEvent&&ce[e.originalEvent.pointerType.toUpperCase()]?t.touchStartX=e.originalEvent.clientX:t._pointerEvent||(t.touchStartX=e.originalEvent.touches[0].clientX)},i=function(e){t._pointerEvent&&ce[e.originalEvent.pointerType.toUpperCase()]&&(t.touchDeltaX=e.originalEvent.clientX-t.touchStartX),t._handleSwipe(),\"hover\"===t._config.pause&&(t.pause(),t.touchTimeout&&clearTimeout(t.touchTimeout),t.touchTimeout=setTimeout(function(e){return t.cycle(e)},500+t._config.interval))};p(this._element.querySelectorAll(oe)).on(V.DRAG_START,function(e){return e.preventDefault()}),this._pointerEvent?(p(this._element).on(V.POINTERDOWN,function(e){return n(e)}),p(this._element).on(V.POINTERUP,function(e){return i(e)}),this._element.classList.add(ee)):(p(this._element).on(V.TOUCHSTART,function(e){return n(e)}),p(this._element).on(V.TOUCHMOVE,function(e){return function(e){e.originalEvent.touches&&1<e.originalEvent.touches.length?t.touchDeltaX=0:t.touchDeltaX=e.originalEvent.touches[0].clientX-t.touchStartX}(e)}),p(this._element).on(V.TOUCHEND,function(e){return i(e)}))}},e._keydown=function(e){if(!/input|textarea/i.test(e.target.tagName))switch(e.which){case 37:e.preventDefault(),this.prev();break;case 39:e.preventDefault(),this.next()}},e._getItemIndex=function(e){return this._items=e&&e.parentNode?[].slice.call(e.parentNode.querySelectorAll(ie)):[],this._items.indexOf(e)},e._getItemByDirection=function(e,t){var n=e===B,i=e===q,o=this._getItemIndex(t),r=this._items.length-1;if((i&&0===o||n&&o===r)&&!this._config.wrap)return t;var s=(o+(e===q?-1:1))%this._items.length;return-1==s?this._items[this._items.length-1]:this._items[s]},e._triggerSlideEvent=function(e,t){var n=this._getItemIndex(e),i=this._getItemIndex(this._element.querySelector(ne)),o=p.Event(V.SLIDE,{relatedTarget:e,direction:t,from:i,to:n});return p(this._element).trigger(o),o},e._setActiveIndicatorElement=function(e){if(this._indicatorsElement){var t=[].slice.call(this._indicatorsElement.querySelectorAll(te));p(t).removeClass(z);var n=this._indicatorsElement.children[this._getItemIndex(e)];n&&p(n).addClass(z)}},e._slide=function(e,t){var n,i,o,r=this,s=this._element.querySelector(ne),a=this._getItemIndex(s),l=t||s&&this._getItemByDirection(e,s),c=this._getItemIndex(l),h=Boolean(this._interval);if(o=e===B?(n=$,i=J,K):(n=G,i=Z,Q),l&&p(l).hasClass(z))this._isSliding=!1;else if(!this._triggerSlideEvent(l,o).isDefaultPrevented()&&s&&l){this._isSliding=!0,h&&this.pause(),this._setActiveIndicatorElement(l);var u=p.Event(V.SLID,{relatedTarget:l,direction:o,from:a,to:c});if(p(this._element).hasClass(X)){p(l).addClass(i),m.reflow(l),p(s).addClass(n),p(l).addClass(n);var f=parseInt(l.getAttribute(\"data-interval\"),10);f?(this._config.defaultInterval=this._config.defaultInterval||this._config.interval,this._config.interval=f):this._config.interval=this._config.defaultInterval||this._config.interval;var d=m.getTransitionDurationFromElement(s);p(s).one(m.TRANSITION_END,function(){p(l).removeClass(n+\" \"+i).addClass(z),p(s).removeClass(z+\" \"+i+\" \"+n),r._isSliding=!1,setTimeout(function(){return p(r._element).trigger(u)},0)}).emulateTransitionEnd(d)}else p(s).removeClass(z),p(l).addClass(z),this._isSliding=!1,p(this._element).trigger(u);h&&this.cycle()}},r._jQueryInterface=function(i){return this.each(function(){var e=p(this).data(H),t=l({},W,{},p(this).data());\"object\"==typeof i&&(t=l({},t,{},i));var n=\"string\"==typeof i?i:t.slide;if(e||(e=new r(this,t),p(this).data(H,e)),\"number\"==typeof i)e.to(i);else if(\"string\"==typeof n){if(\"undefined\"==typeof e[n])throw new TypeError('No method named \"'+n+'\"');e[n]()}else t.interval&&t.ride&&(e.pause(),e.cycle())})},r._dataApiClickHandler=function(e){var t=m.getSelectorFromElement(this);if(t){var n=p(t)[0];if(n&&p(n).hasClass(Y)){var i=l({},p(n).data(),{},p(this).data()),o=this.getAttribute(\"data-slide-to\");o&&(i.interval=!1),r._jQueryInterface.call(p(n),i),o&&p(n).data(H).to(o),e.preventDefault()}}},s(r,null,[{key:\"VERSION\",get:function(){return\"4.4.1\"}},{key:\"Default\",get:function(){return W}}]),r}();p(document).on(V.CLICK_DATA_API,ae,he._dataApiClickHandler),p(window).on(V.LOAD_DATA_API,function(){for(var e=[].slice.call(document.querySelectorAll(le)),t=0,n=e.length;t<n;t++){var i=p(e[t]);he._jQueryInterface.call(i,i.data())}}),p.fn[j]=he._jQueryInterface,p.fn[j].Constructor=he,p.fn[j].noConflict=function(){return p.fn[j]=M,he._jQueryInterface};var ue=\"collapse\",fe=\"bs.collapse\",de=\".\"+fe,pe=p.fn[ue],me={toggle:!0,parent:\"\"},ge={toggle:\"boolean\",parent:\"(string|element)\"},_e={SHOW:\"show\"+de,SHOWN:\"shown\"+de,HIDE:\"hide\"+de,HIDDEN:\"hidden\"+de,CLICK_DATA_API:\"click\"+de+\".data-api\"},ve=\"show\",ye=\"collapse\",Ee=\"collapsing\",be=\"collapsed\",we=\"width\",Te=\"height\",Ce=\".show, .collapsing\",Se='[data-toggle=\"collapse\"]',De=function(){function a(t,e){this._isTransitioning=!1,this._element=t,this._config=this._getConfig(e),this._triggerArray=[].slice.call(document.querySelectorAll('[data-toggle=\"collapse\"][href=\"#'+t.id+'\"],[data-toggle=\"collapse\"][data-target=\"#'+t.id+'\"]'));for(var n=[].slice.call(document.querySelectorAll(Se)),i=0,o=n.length;i<o;i++){var r=n[i],s=m.getSelectorFromElement(r),a=[].slice.call(document.querySelectorAll(s)).filter(function(e){return e===t});null!==s&&0<a.length&&(this._selector=s,this._triggerArray.push(r))}this._parent=this._config.parent?this._getParent():null,this._config.parent||this._addAriaAndCollapsedClass(this._element,this._triggerArray),this._config.toggle&&this.toggle()}var e=a.prototype;return e.toggle=function(){p(this._element).hasClass(ve)?this.hide():this.show()},e.show=function(){var e,t,n=this;if(!this._isTransitioning&&!p(this._element).hasClass(ve)&&(this._parent&&0===(e=[].slice.call(this._parent.querySelectorAll(Ce)).filter(function(e){return\"string\"==typeof n._config.parent?e.getAttribute(\"data-parent\")===n._config.parent:e.classList.contains(ye)})).length&&(e=null),!(e&&(t=p(e).not(this._selector).data(fe))&&t._isTransitioning))){var i=p.Event(_e.SHOW);if(p(this._element).trigger(i),!i.isDefaultPrevented()){e&&(a._jQueryInterface.call(p(e).not(this._selector),\"hide\"),t||p(e).data(fe,null));var o=this._getDimension();p(this._element).removeClass(ye).addClass(Ee),this._element.style[o]=0,this._triggerArray.length&&p(this._triggerArray).removeClass(be).attr(\"aria-expanded\",!0),this.setTransitioning(!0);var r=\"scroll\"+(o[0].toUpperCase()+o.slice(1)),s=m.getTransitionDurationFromElement(this._element);p(this._element).one(m.TRANSITION_END,function(){p(n._element).removeClass(Ee).addClass(ye).addClass(ve),n._element.style[o]=\"\",n.setTransitioning(!1),p(n._element).trigger(_e.SHOWN)}).emulateTransitionEnd(s),this._element.style[o]=this._element[r]+\"px\"}}},e.hide=function(){var e=this;if(!this._isTransitioning&&p(this._element).hasClass(ve)){var t=p.Event(_e.HIDE);if(p(this._element).trigger(t),!t.isDefaultPrevented()){var n=this._getDimension();this._element.style[n]=this._element.getBoundingClientRect()[n]+\"px\",m.reflow(this._element),p(this._element).addClass(Ee).removeClass(ye).removeClass(ve);var i=this._triggerArray.length;if(0<i)for(var o=0;o<i;o++){var r=this._triggerArray[o],s=m.getSelectorFromElement(r);if(null!==s)p([].slice.call(document.querySelectorAll(s))).hasClass(ve)||p(r).addClass(be).attr(\"aria-expanded\",!1)}this.setTransitioning(!0);this._element.style[n]=\"\";var a=m.getTransitionDurationFromElement(this._element);p(this._element).one(m.TRANSITION_END,function(){e.setTransitioning(!1),p(e._element).removeClass(Ee).addClass(ye).trigger(_e.HIDDEN)}).emulateTransitionEnd(a)}}},e.setTransitioning=function(e){this._isTransitioning=e},e.dispose=function(){p.removeData(this._element,fe),this._config=null,this._parent=null,this._element=null,this._triggerArray=null,this._isTransitioning=null},e._getConfig=function(e){return(e=l({},me,{},e)).toggle=Boolean(e.toggle),m.typeCheckConfig(ue,e,ge),e},e._getDimension=function(){return p(this._element).hasClass(we)?we:Te},e._getParent=function(){var e,n=this;m.isElement(this._config.parent)?(e=this._config.parent,\"undefined\"!=typeof this._config.parent.jquery&&(e=this._config.parent[0])):e=document.querySelector(this._config.parent);var t='[data-toggle=\"collapse\"][data-parent=\"'+this._config.parent+'\"]',i=[].slice.call(e.querySelectorAll(t));return p(i).each(function(e,t){n._addAriaAndCollapsedClass(a._getTargetFromElement(t),[t])}),e},e._addAriaAndCollapsedClass=function(e,t){var n=p(e).hasClass(ve);t.length&&p(t).toggleClass(be,!n).attr(\"aria-expanded\",n)},a._getTargetFromElement=function(e){var t=m.getSelectorFromElement(e);return t?document.querySelector(t):null},a._jQueryInterface=function(i){return this.each(function(){var e=p(this),t=e.data(fe),n=l({},me,{},e.data(),{},\"object\"==typeof i&&i?i:{});if(!t&&n.toggle&&/show|hide/.test(i)&&(n.toggle=!1),t||(t=new a(this,n),e.data(fe,t)),\"string\"==typeof i){if(\"undefined\"==typeof t[i])throw new TypeError('No method named \"'+i+'\"');t[i]()}})},s(a,null,[{key:\"VERSION\",get:function(){return\"4.4.1\"}},{key:\"Default\",get:function(){return me}}]),a}();p(document).on(_e.CLICK_DATA_API,Se,function(e){\"A\"===e.currentTarget.tagName&&e.preventDefault();var n=p(this),t=m.getSelectorFromElement(this),i=[].slice.call(document.querySelectorAll(t));p(i).each(function(){var e=p(this),t=e.data(fe)?\"toggle\":n.data();De._jQueryInterface.call(e,t)})}),p.fn[ue]=De._jQueryInterface,p.fn[ue].Constructor=De,p.fn[ue].noConflict=function(){return p.fn[ue]=pe,De._jQueryInterface};var Ie=\"undefined\"!=typeof window&&\"undefined\"!=typeof document&&\"undefined\"!=typeof navigator,Ae=function(){for(var e=[\"Edge\",\"Trident\",\"Firefox\"],t=0;t<e.length;t+=1)if(Ie&&0<=navigator.userAgent.indexOf(e[t]))return 1;return 0}();var Oe=Ie&&window.Promise?function(e){var t=!1;return function(){t||(t=!0,window.Promise.resolve().then(function(){t=!1,e()}))}}:function(e){var t=!1;return function(){t||(t=!0,setTimeout(function(){t=!1,e()},Ae))}};function Ne(e){return e&&\"[object Function]\"==={}.toString.call(e)}function ke(e,t){if(1!==e.nodeType)return[];var n=e.ownerDocument.defaultView.getComputedStyle(e,null);return t?n[t]:n}function Le(e){return\"HTML\"===e.nodeName?e:e.parentNode||e.host}function Pe(e){if(!e)return document.body;switch(e.nodeName){case\"HTML\":case\"BODY\":return e.ownerDocument.body;case\"#document\":return e.body}var t=ke(e),n=t.overflow,i=t.overflowX,o=t.overflowY;return/(auto|scroll|overlay)/.test(n+o+i)?e:Pe(Le(e))}function xe(e){return e&&e.referenceNode?e.referenceNode:e}var je=Ie&&!(!window.MSInputMethodContext||!document.documentMode),He=Ie&&/MSIE 10/.test(navigator.userAgent);function Re(e){return 11===e?je:10===e?He:je||He}function Fe(e){if(!e)return document.documentElement;for(var t=Re(10)?document.body:null,n=e.offsetParent||null;n===t&&e.nextElementSibling;)n=(e=e.nextElementSibling).offsetParent;var i=n&&n.nodeName;return i&&\"BODY\"!==i&&\"HTML\"!==i?-1!==[\"TH\",\"TD\",\"TABLE\"].indexOf(n.nodeName)&&\"static\"===ke(n,\"position\")?Fe(n):n:e?e.ownerDocument.documentElement:document.documentElement}function Me(e){return null!==e.parentNode?Me(e.parentNode):e}function We(e,t){if(!(e&&e.nodeType&&t&&t.nodeType))return document.documentElement;var n=e.compareDocumentPosition(t)&Node.DOCUMENT_POSITION_FOLLOWING,i=n?e:t,o=n?t:e,r=document.createRange();r.setStart(i,0),r.setEnd(o,0);var s=r.commonAncestorContainer;if(e!==s&&t!==s||i.contains(o))return function(e){var t=e.nodeName;return\"BODY\"!==t&&(\"HTML\"===t||Fe(e.firstElementChild)===e)}(s)?s:Fe(s);var a=Me(e);return a.host?We(a.host,t):We(e,Me(t).host)}function Ue(e,t){var n=\"top\"===(1<arguments.length&&void 0!==t?t:\"top\")?\"scrollTop\":\"scrollLeft\",i=e.nodeName;if(\"BODY\"!==i&&\"HTML\"!==i)return e[n];var o=e.ownerDocument.documentElement;return(e.ownerDocument.scrollingElement||o)[n]}function Be(e,t){var n=\"x\"===t?\"Left\":\"Top\",i=\"Left\"==n?\"Right\":\"Bottom\";return parseFloat(e[\"border\"+n+\"Width\"],10)+parseFloat(e[\"border\"+i+\"Width\"],10)}function qe(e,t,n,i){return Math.max(t[\"offset\"+e],t[\"scroll\"+e],n[\"client\"+e],n[\"offset\"+e],n[\"scroll\"+e],Re(10)?parseInt(n[\"offset\"+e])+parseInt(i[\"margin\"+(\"Height\"===e?\"Top\":\"Left\")])+parseInt(i[\"margin\"+(\"Height\"===e?\"Bottom\":\"Right\")]):0)}function Ke(e){var t=e.body,n=e.documentElement,i=Re(10)&&getComputedStyle(n);return{height:qe(\"Height\",t,n,i),width:qe(\"Width\",t,n,i)}}var Qe=function(e,t,n){return t&&Ve(e.prototype,t),n&&Ve(e,n),e};function Ve(e,t){for(var n=0;n<t.length;n++){var i=t[n];i.enumerable=i.enumerable||!1,i.configurable=!0,\"value\"in i&&(i.writable=!0),Object.defineProperty(e,i.key,i)}}function Ye(e,t,n){return t in e?Object.defineProperty(e,t,{value:n,enumerable:!0,configurable:!0,writable:!0}):e[t]=n,e}var ze=Object.assign||function(e){for(var t=1;t<arguments.length;t++){var n=arguments[t];for(var i in n)Object.prototype.hasOwnProperty.call(n,i)&&(e[i]=n[i])}return e};function Xe(e){return ze({},e,{right:e.left+e.width,bottom:e.top+e.height})}function Ge(e){var t={};try{if(Re(10)){t=e.getBoundingClientRect();var n=Ue(e,\"top\"),i=Ue(e,\"left\");t.top+=n,t.left+=i,t.bottom+=n,t.right+=i}else t=e.getBoundingClientRect()}catch(e){}var o={left:t.left,top:t.top,width:t.right-t.left,height:t.bottom-t.top},r=\"HTML\"===e.nodeName?Ke(e.ownerDocument):{},s=r.width||e.clientWidth||o.width,a=r.height||e.clientHeight||o.height,l=e.offsetWidth-s,c=e.offsetHeight-a;if(l||c){var h=ke(e);l-=Be(h,\"x\"),c-=Be(h,\"y\"),o.width-=l,o.height-=c}return Xe(o)}function $e(e,t,n){var i=2<arguments.length&&void 0!==n&&n,o=Re(10),r=\"HTML\"===t.nodeName,s=Ge(e),a=Ge(t),l=Pe(e),c=ke(t),h=parseFloat(c.borderTopWidth,10),u=parseFloat(c.borderLeftWidth,10);i&&r&&(a.top=Math.max(a.top,0),a.left=Math.max(a.left,0));var f=Xe({top:s.top-a.top-h,left:s.left-a.left-u,width:s.width,height:s.height});if(f.marginTop=0,f.marginLeft=0,!o&&r){var d=parseFloat(c.marginTop,10),p=parseFloat(c.marginLeft,10);f.top-=h-d,f.bottom-=h-d,f.left-=u-p,f.right-=u-p,f.marginTop=d,f.marginLeft=p}return(o&&!i?t.contains(l):t===l&&\"BODY\"!==l.nodeName)&&(f=function(e,t,n){var i=2<arguments.length&&void 0!==n&&n,o=Ue(t,\"top\"),r=Ue(t,\"left\"),s=i?-1:1;return e.top+=o*s,e.bottom+=o*s,e.left+=r*s,e.right+=r*s,e}(f,t)),f}function Je(e){if(!e||!e.parentElement||Re())return document.documentElement;for(var t=e.parentElement;t&&\"none\"===ke(t,\"transform\");)t=t.parentElement;return t||document.documentElement}function Ze(e,t,n,i,o){var r=4<arguments.length&&void 0!==o&&o,s={top:0,left:0},a=r?Je(e):We(e,xe(t));if(\"viewport\"===i)s=function(e,t){var n=1<arguments.length&&void 0!==t&&t,i=e.ownerDocument.documentElement,o=$e(e,i),r=Math.max(i.clientWidth,window.innerWidth||0),s=Math.max(i.clientHeight,window.innerHeight||0),a=n?0:Ue(i),l=n?0:Ue(i,\"left\");return Xe({top:a-o.top+o.marginTop,left:l-o.left+o.marginLeft,width:r,height:s})}(a,r);else{var l=void 0;\"scrollParent\"===i?\"BODY\"===(l=Pe(Le(t))).nodeName&&(l=e.ownerDocument.documentElement):l=\"window\"===i?e.ownerDocument.documentElement:i;var c=$e(l,a,r);if(\"HTML\"!==l.nodeName||function e(t){var n=t.nodeName;if(\"BODY\"===n||\"HTML\"===n)return!1;if(\"fixed\"===ke(t,\"position\"))return!0;var i=Le(t);return!!i&&e(i)}(a))s=c;else{var h=Ke(e.ownerDocument),u=h.height,f=h.width;s.top+=c.top-c.marginTop,s.bottom=u+c.top,s.left+=c.left-c.marginLeft,s.right=f+c.left}}var d=\"number\"==typeof(n=n||0);return s.left+=d?n:n.left||0,s.top+=d?n:n.top||0,s.right-=d?n:n.right||0,s.bottom-=d?n:n.bottom||0,s}function et(e,t,i,n,o,r){var s=5<arguments.length&&void 0!==r?r:0;if(-1===e.indexOf(\"auto\"))return e;var a=Ze(i,n,s,o),l={top:{width:a.width,height:t.top-a.top},right:{width:a.right-t.right,height:a.height},bottom:{width:a.width,height:a.bottom-t.bottom},left:{width:t.left-a.left,height:a.height}},c=Object.keys(l).map(function(e){return ze({key:e},l[e],{area:function(e){return e.width*e.height}(l[e])})}).sort(function(e,t){return t.area-e.area}),h=c.filter(function(e){var t=e.width,n=e.height;return t>=i.clientWidth&&n>=i.clientHeight}),u=0<h.length?h[0].key:c[0].key,f=e.split(\"-\")[1];return u+(f?\"-\"+f:\"\")}function tt(e,t,n,i){var o=3<arguments.length&&void 0!==i?i:null;return $e(n,o?Je(t):We(t,xe(n)),o)}function nt(e){var t=e.ownerDocument.defaultView.getComputedStyle(e),n=parseFloat(t.marginTop||0)+parseFloat(t.marginBottom||0),i=parseFloat(t.marginLeft||0)+parseFloat(t.marginRight||0);return{width:e.offsetWidth+i,height:e.offsetHeight+n}}function it(e){var t={left:\"right\",right:\"left\",bottom:\"top\",top:\"bottom\"};return e.replace(/left|right|bottom|top/g,function(e){return t[e]})}function ot(e,t,n){n=n.split(\"-\")[0];var i=nt(e),o={width:i.width,height:i.height},r=-1!==[\"right\",\"left\"].indexOf(n),s=r?\"top\":\"left\",a=r?\"left\":\"top\",l=r?\"height\":\"width\",c=r?\"width\":\"height\";return o[s]=t[s]+t[l]/2-i[l]/2,o[a]=n===a?t[a]-i[c]:t[it(a)],o}function rt(e,t){return Array.prototype.find?e.find(t):e.filter(t)[0]}function st(e,n,t){return(void 0===t?e:e.slice(0,function(e,t,n){if(Array.prototype.findIndex)return e.findIndex(function(e){return e[t]===n});var i=rt(e,function(e){return e[t]===n});return e.indexOf(i)}(e,\"name\",t))).forEach(function(e){e.function&&console.warn(\"`modifier.function` is deprecated, use `modifier.fn`!\");var t=e.function||e.fn;e.enabled&&Ne(t)&&(n.offsets.popper=Xe(n.offsets.popper),n.offsets.reference=Xe(n.offsets.reference),n=t(n,e))}),n}function at(e,n){return e.some(function(e){var t=e.name;return e.enabled&&t===n})}function lt(e){for(var t=[!1,\"ms\",\"Webkit\",\"Moz\",\"O\"],n=e.charAt(0).toUpperCase()+e.slice(1),i=0;i<t.length;i++){var o=t[i],r=o?\"\"+o+n:e;if(\"undefined\"!=typeof document.body.style[r])return r}return null}function ct(e){var t=e.ownerDocument;return t?t.defaultView:window}function ht(e,t,n,i){n.updateBound=i,ct(e).addEventListener(\"resize\",n.updateBound,{passive:!0});var o=Pe(e);return function e(t,n,i,o){var r=\"BODY\"===t.nodeName,s=r?t.ownerDocument.defaultView:t;s.addEventListener(n,i,{passive:!0}),r||e(Pe(s.parentNode),n,i,o),o.push(s)}(o,\"scroll\",n.updateBound,n.scrollParents),n.scrollElement=o,n.eventsEnabled=!0,n}function ut(){this.state.eventsEnabled&&(cancelAnimationFrame(this.scheduleUpdate),this.state=function(e,t){return ct(e).removeEventListener(\"resize\",t.updateBound),t.scrollParents.forEach(function(e){e.removeEventListener(\"scroll\",t.updateBound)}),t.updateBound=null,t.scrollParents=[],t.scrollElement=null,t.eventsEnabled=!1,t}(this.reference,this.state))}function ft(e){return\"\"!==e&&!isNaN(parseFloat(e))&&isFinite(e)}function dt(n,i){Object.keys(i).forEach(function(e){var t=\"\";-1!==[\"width\",\"height\",\"top\",\"right\",\"bottom\",\"left\"].indexOf(e)&&ft(i[e])&&(t=\"px\"),n.style[e]=i[e]+t})}function pt(e,t){function n(e){return e}var i=e.offsets,o=i.popper,r=i.reference,s=Math.round,a=Math.floor,l=s(r.width),c=s(o.width),h=-1!==[\"left\",\"right\"].indexOf(e.placement),u=-1!==e.placement.indexOf(\"-\"),f=t?h||u||l%2==c%2?s:a:n,d=t?s:n;return{left:f(l%2==1&&c%2==1&&!u&&t?o.left-1:o.left),top:d(o.top),bottom:d(o.bottom),right:f(o.right)}}var mt=Ie&&/Firefox/i.test(navigator.userAgent);function gt(e,t,n){var i=rt(e,function(e){return e.name===t}),o=!!i&&e.some(function(e){return e.name===n&&e.enabled&&e.order<i.order});if(!o){var r=\"`\"+t+\"`\",s=\"`\"+n+\"`\";console.warn(s+\" modifier is required by \"+r+\" modifier in order to work, be sure to include it before \"+r+\"!\")}return o}var _t=[\"auto-start\",\"auto\",\"auto-end\",\"top-start\",\"top\",\"top-end\",\"right-start\",\"right\",\"right-end\",\"bottom-end\",\"bottom\",\"bottom-start\",\"left-end\",\"left\",\"left-start\"],vt=_t.slice(3);function yt(e,t){var n=1<arguments.length&&void 0!==t&&t,i=vt.indexOf(e),o=vt.slice(i+1).concat(vt.slice(0,i));return n?o.reverse():o}var Et=\"flip\",bt=\"clockwise\",wt=\"counterclockwise\";function Tt(e,o,r,t){var s=[0,0],a=-1!==[\"right\",\"left\"].indexOf(t),n=e.split(/(\\+|\\-)/).map(function(e){return e.trim()}),i=n.indexOf(rt(n,function(e){return-1!==e.search(/,|\\s/)}));n[i]&&-1===n[i].indexOf(\",\")&&console.warn(\"Offsets separated by white space(s) are deprecated, use a comma (,) instead.\");var l=/\\s*,\\s*|\\s+/,c=-1!==i?[n.slice(0,i).concat([n[i].split(l)[0]]),[n[i].split(l)[1]].concat(n.slice(i+1))]:[n];return(c=c.map(function(e,t){var n=(1===t?!a:a)?\"height\":\"width\",i=!1;return e.reduce(function(e,t){return\"\"===e[e.length-1]&&-1!==[\"+\",\"-\"].indexOf(t)?(e[e.length-1]=t,i=!0,e):i?(e[e.length-1]+=t,i=!1,e):e.concat(t)},[]).map(function(e){return function(e,t,n,i){var o=e.match(/((?:\\-|\\+)?\\d*\\.?\\d*)(.*)/),r=+o[1],s=o[2];if(!r)return e;if(0!==s.indexOf(\"%\"))return\"vh\"!==s&&\"vw\"!==s?r:(\"vh\"===s?Math.max(document.documentElement.clientHeight,window.innerHeight||0):Math.max(document.documentElement.clientWidth,window.innerWidth||0))/100*r;var a=void 0;switch(s){case\"%p\":a=n;break;case\"%\":case\"%r\":default:a=i}return Xe(a)[t]/100*r}(e,n,o,r)})})).forEach(function(n,i){n.forEach(function(e,t){ft(e)&&(s[i]+=e*(\"-\"===n[t-1]?-1:1))})}),s}var Ct={placement:\"bottom\",positionFixed:!1,eventsEnabled:!0,removeOnDestroy:!1,onCreate:function(){},onUpdate:function(){},modifiers:{shift:{order:100,enabled:!0,fn:function(e){var t=e.placement,n=t.split(\"-\")[0],i=t.split(\"-\")[1];if(i){var o=e.offsets,r=o.reference,s=o.popper,a=-1!==[\"bottom\",\"top\"].indexOf(n),l=a?\"left\":\"top\",c=a?\"width\":\"height\",h={start:Ye({},l,r[l]),end:Ye({},l,r[l]+r[c]-s[c])};e.offsets.popper=ze({},s,h[i])}return e}},offset:{order:200,enabled:!0,fn:function(e,t){var n=t.offset,i=e.placement,o=e.offsets,r=o.popper,s=o.reference,a=i.split(\"-\")[0],l=void 0;return l=ft(+n)?[+n,0]:Tt(n,r,s,a),\"left\"===a?(r.top+=l[0],r.left-=l[1]):\"right\"===a?(r.top+=l[0],r.left+=l[1]):\"top\"===a?(r.left+=l[0],r.top-=l[1]):\"bottom\"===a&&(r.left+=l[0],r.top+=l[1]),e.popper=r,e},offset:0},preventOverflow:{order:300,enabled:!0,fn:function(e,i){var t=i.boundariesElement||Fe(e.instance.popper);e.instance.reference===t&&(t=Fe(t));var n=lt(\"transform\"),o=e.instance.popper.style,r=o.top,s=o.left,a=o[n];o.top=\"\",o.left=\"\",o[n]=\"\";var l=Ze(e.instance.popper,e.instance.reference,i.padding,t,e.positionFixed);o.top=r,o.left=s,o[n]=a,i.boundaries=l;var c=i.priority,h=e.offsets.popper,u={primary:function(e){var t=h[e];return h[e]<l[e]&&!i.escapeWithReference&&(t=Math.max(h[e],l[e])),Ye({},e,t)},secondary:function(e){var t=\"right\"===e?\"left\":\"top\",n=h[t];return h[e]>l[e]&&!i.escapeWithReference&&(n=Math.min(h[t],l[e]-(\"right\"===e?h.width:h.height))),Ye({},t,n)}};return c.forEach(function(e){var t=-1!==[\"left\",\"top\"].indexOf(e)?\"primary\":\"secondary\";h=ze({},h,u[t](e))}),e.offsets.popper=h,e},priority:[\"left\",\"right\",\"top\",\"bottom\"],padding:5,boundariesElement:\"scrollParent\"},keepTogether:{order:400,enabled:!0,fn:function(e){var t=e.offsets,n=t.popper,i=t.reference,o=e.placement.split(\"-\")[0],r=Math.floor,s=-1!==[\"top\",\"bottom\"].indexOf(o),a=s?\"right\":\"bottom\",l=s?\"left\":\"top\",c=s?\"width\":\"height\";return n[a]<r(i[l])&&(e.offsets.popper[l]=r(i[l])-n[c]),n[l]>r(i[a])&&(e.offsets.popper[l]=r(i[a])),e}},arrow:{order:500,enabled:!0,fn:function(e,t){var n;if(!gt(e.instance.modifiers,\"arrow\",\"keepTogether\"))return e;var i=t.element;if(\"string\"==typeof i){if(!(i=e.instance.popper.querySelector(i)))return e}else if(!e.instance.popper.contains(i))return console.warn(\"WARNING: `arrow.element` must be child of its popper element!\"),e;var o=e.placement.split(\"-\")[0],r=e.offsets,s=r.popper,a=r.reference,l=-1!==[\"left\",\"right\"].indexOf(o),c=l?\"height\":\"width\",h=l?\"Top\":\"Left\",u=h.toLowerCase(),f=l?\"left\":\"top\",d=l?\"bottom\":\"right\",p=nt(i)[c];a[d]-p<s[u]&&(e.offsets.popper[u]-=s[u]-(a[d]-p)),a[u]+p>s[d]&&(e.offsets.popper[u]+=a[u]+p-s[d]),e.offsets.popper=Xe(e.offsets.popper);var m=a[u]+a[c]/2-p/2,g=ke(e.instance.popper),_=parseFloat(g[\"margin\"+h],10),v=parseFloat(g[\"border\"+h+\"Width\"],10),y=m-e.offsets.popper[u]-_-v;return y=Math.max(Math.min(s[c]-p,y),0),e.arrowElement=i,e.offsets.arrow=(Ye(n={},u,Math.round(y)),Ye(n,f,\"\"),n),e},element:\"[x-arrow]\"},flip:{order:600,enabled:!0,fn:function(m,g){if(at(m.instance.modifiers,\"inner\"))return m;if(m.flipped&&m.placement===m.originalPlacement)return m;var _=Ze(m.instance.popper,m.instance.reference,g.padding,g.boundariesElement,m.positionFixed),v=m.placement.split(\"-\")[0],y=it(v),E=m.placement.split(\"-\")[1]||\"\",b=[];switch(g.behavior){case Et:b=[v,y];break;case bt:b=yt(v);break;case wt:b=yt(v,!0);break;default:b=g.behavior}return b.forEach(function(e,t){if(v!==e||b.length===t+1)return m;v=m.placement.split(\"-\")[0],y=it(v);var n=m.offsets.popper,i=m.offsets.reference,o=Math.floor,r=\"left\"===v&&o(n.right)>o(i.left)||\"right\"===v&&o(n.left)<o(i.right)||\"top\"===v&&o(n.bottom)>o(i.top)||\"bottom\"===v&&o(n.top)<o(i.bottom),s=o(n.left)<o(_.left),a=o(n.right)>o(_.right),l=o(n.top)<o(_.top),c=o(n.bottom)>o(_.bottom),h=\"left\"===v&&s||\"right\"===v&&a||\"top\"===v&&l||\"bottom\"===v&&c,u=-1!==[\"top\",\"bottom\"].indexOf(v),f=!!g.flipVariations&&(u&&\"start\"===E&&s||u&&\"end\"===E&&a||!u&&\"start\"===E&&l||!u&&\"end\"===E&&c),d=!!g.flipVariationsByContent&&(u&&\"start\"===E&&a||u&&\"end\"===E&&s||!u&&\"start\"===E&&c||!u&&\"end\"===E&&l),p=f||d;(r||h||p)&&(m.flipped=!0,(r||h)&&(v=b[t+1]),p&&(E=function(e){return\"end\"===e?\"start\":\"start\"===e?\"end\":e}(E)),m.placement=v+(E?\"-\"+E:\"\"),m.offsets.popper=ze({},m.offsets.popper,ot(m.instance.popper,m.offsets.reference,m.placement)),m=st(m.instance.modifiers,m,\"flip\"))}),m},behavior:\"flip\",padding:5,boundariesElement:\"viewport\",flipVariations:!1,flipVariationsByContent:!1},inner:{order:700,enabled:!1,fn:function(e){var t=e.placement,n=t.split(\"-\")[0],i=e.offsets,o=i.popper,r=i.reference,s=-1!==[\"left\",\"right\"].indexOf(n),a=-1===[\"top\",\"left\"].indexOf(n);return o[s?\"left\":\"top\"]=r[n]-(a?o[s?\"width\":\"height\"]:0),e.placement=it(t),e.offsets.popper=Xe(o),e}},hide:{order:800,enabled:!0,fn:function(e){if(!gt(e.instance.modifiers,\"hide\",\"preventOverflow\"))return e;var t=e.offsets.reference,n=rt(e.instance.modifiers,function(e){return\"preventOverflow\"===e.name}).boundaries;if(t.bottom<n.top||t.left>n.right||t.top>n.bottom||t.right<n.left){if(!0===e.hide)return e;e.hide=!0,e.attributes[\"x-out-of-boundaries\"]=\"\"}else{if(!1===e.hide)return e;e.hide=!1,e.attributes[\"x-out-of-boundaries\"]=!1}return e}},computeStyle:{order:850,enabled:!0,fn:function(e,t){var n=t.x,i=t.y,o=e.offsets.popper,r=rt(e.instance.modifiers,function(e){return\"applyStyle\"===e.name}).gpuAcceleration;void 0!==r&&console.warn(\"WARNING: `gpuAcceleration` option moved to `computeStyle` modifier and will not be supported in future versions of Popper.js!\");var s=void 0!==r?r:t.gpuAcceleration,a=Fe(e.instance.popper),l=Ge(a),c={position:o.position},h=pt(e,window.devicePixelRatio<2||!mt),u=\"bottom\"===n?\"top\":\"bottom\",f=\"right\"===i?\"left\":\"right\",d=lt(\"transform\"),p=void 0,m=void 0;if(m=\"bottom\"==u?\"HTML\"===a.nodeName?-a.clientHeight+h.bottom:-l.height+h.bottom:h.top,p=\"right\"==f?\"HTML\"===a.nodeName?-a.clientWidth+h.right:-l.width+h.right:h.left,s&&d)c[d]=\"translate3d(\"+p+\"px, \"+m+\"px, 0)\",c[u]=0,c[f]=0,c.willChange=\"transform\";else{var g=\"bottom\"==u?-1:1,_=\"right\"==f?-1:1;c[u]=m*g,c[f]=p*_,c.willChange=u+\", \"+f}var v={\"x-placement\":e.placement};return e.attributes=ze({},v,e.attributes),e.styles=ze({},c,e.styles),e.arrowStyles=ze({},e.offsets.arrow,e.arrowStyles),e},gpuAcceleration:!0,x:\"bottom\",y:\"right\"},applyStyle:{order:900,enabled:!0,fn:function(e){return dt(e.instance.popper,e.styles),function(t,n){Object.keys(n).forEach(function(e){!1!==n[e]?t.setAttribute(e,n[e]):t.removeAttribute(e)})}(e.instance.popper,e.attributes),e.arrowElement&&Object.keys(e.arrowStyles).length&&dt(e.arrowElement,e.arrowStyles),e},onLoad:function(e,t,n,i,o){var r=tt(o,t,e,n.positionFixed),s=et(n.placement,r,t,e,n.modifiers.flip.boundariesElement,n.modifiers.flip.padding);return t.setAttribute(\"x-placement\",s),dt(t,{position:n.positionFixed?\"fixed\":\"absolute\"}),n},gpuAcceleration:void 0}}},St=(Qe(Dt,[{key:\"update\",value:function(){return function(){if(!this.state.isDestroyed){var e={instance:this,styles:{},arrowStyles:{},attributes:{},flipped:!1,offsets:{}};e.offsets.reference=tt(this.state,this.popper,this.reference,this.options.positionFixed),e.placement=et(this.options.placement,e.offsets.reference,this.popper,this.reference,this.options.modifiers.flip.boundariesElement,this.options.modifiers.flip.padding),e.originalPlacement=e.placement,e.positionFixed=this.options.positionFixed,e.offsets.popper=ot(this.popper,e.offsets.reference,e.placement),e.offsets.popper.position=this.options.positionFixed?\"fixed\":\"absolute\",e=st(this.modifiers,e),this.state.isCreated?this.options.onUpdate(e):(this.state.isCreated=!0,this.options.onCreate(e))}}.call(this)}},{key:\"destroy\",value:function(){return function(){return this.state.isDestroyed=!0,at(this.modifiers,\"applyStyle\")&&(this.popper.removeAttribute(\"x-placement\"),this.popper.style.position=\"\",this.popper.style.top=\"\",this.popper.style.left=\"\",this.popper.style.right=\"\",this.popper.style.bottom=\"\",this.popper.style.willChange=\"\",this.popper.style[lt(\"transform\")]=\"\"),this.disableEventListeners(),this.options.removeOnDestroy&&this.popper.parentNode.removeChild(this.popper),this}.call(this)}},{key:\"enableEventListeners\",value:function(){return function(){this.state.eventsEnabled||(this.state=ht(this.reference,this.options,this.state,this.scheduleUpdate))}.call(this)}},{key:\"disableEventListeners\",value:function(){return ut.call(this)}}]),Dt);function Dt(e,t){var n=this,i=2<arguments.length&&void 0!==arguments[2]?arguments[2]:{};!function(e,t){if(!(e instanceof t))throw new TypeError(\"Cannot call a class as a function\")}(this,Dt),this.scheduleUpdate=function(){return requestAnimationFrame(n.update)},this.update=Oe(this.update.bind(this)),this.options=ze({},Dt.Defaults,i),this.state={isDestroyed:!1,isCreated:!1,scrollParents:[]},this.reference=e&&e.jquery?e[0]:e,this.popper=t&&t.jquery?t[0]:t,this.options.modifiers={},Object.keys(ze({},Dt.Defaults.modifiers,i.modifiers)).forEach(function(e){n.options.modifiers[e]=ze({},Dt.Defaults.modifiers[e]||{},i.modifiers?i.modifiers[e]:{})}),this.modifiers=Object.keys(this.options.modifiers).map(function(e){return ze({name:e},n.options.modifiers[e])}).sort(function(e,t){return e.order-t.order}),this.modifiers.forEach(function(e){e.enabled&&Ne(e.onLoad)&&e.onLoad(n.reference,n.popper,n.options,e,n.state)}),this.update();var o=this.options.eventsEnabled;o&&this.enableEventListeners(),this.state.eventsEnabled=o}St.Utils=(\"undefined\"!=typeof window?window:global).PopperUtils,St.placements=_t,St.Defaults=Ct;var It=\"dropdown\",At=\"bs.dropdown\",Ot=\".\"+At,Nt=\".data-api\",kt=p.fn[It],Lt=new RegExp(\"38|40|27\"),Pt={HIDE:\"hide\"+Ot,HIDDEN:\"hidden\"+Ot,SHOW:\"show\"+Ot,SHOWN:\"shown\"+Ot,CLICK:\"click\"+Ot,CLICK_DATA_API:\"click\"+Ot+Nt,KEYDOWN_DATA_API:\"keydown\"+Ot+Nt,KEYUP_DATA_API:\"keyup\"+Ot+Nt},xt=\"disabled\",jt=\"show\",Ht=\"dropup\",Rt=\"dropright\",Ft=\"dropleft\",Mt=\"dropdown-menu-right\",Wt=\"position-static\",Ut='[data-toggle=\"dropdown\"]',Bt=\".dropdown form\",qt=\".dropdown-menu\",Kt=\".navbar-nav\",Qt=\".dropdown-menu .dropdown-item:not(.disabled):not(:disabled)\",Vt=\"top-start\",Yt=\"top-end\",zt=\"bottom-start\",Xt=\"bottom-end\",Gt=\"right-start\",$t=\"left-start\",Jt={offset:0,flip:!0,boundary:\"scrollParent\",reference:\"toggle\",display:\"dynamic\",popperConfig:null},Zt={offset:\"(number|string|function)\",flip:\"boolean\",boundary:\"(string|element)\",reference:\"(string|element)\",display:\"string\",popperConfig:\"(null|object)\"},en=function(){function c(e,t){this._element=e,this._popper=null,this._config=this._getConfig(t),this._menu=this._getMenuElement(),this._inNavbar=this._detectNavbar(),this._addEventListeners()}var e=c.prototype;return e.toggle=function(){if(!this._element.disabled&&!p(this._element).hasClass(xt)){var e=p(this._menu).hasClass(jt);c._clearMenus(),e||this.show(!0)}},e.show=function(e){if(void 0===e&&(e=!1),!(this._element.disabled||p(this._element).hasClass(xt)||p(this._menu).hasClass(jt))){var t={relatedTarget:this._element},n=p.Event(Pt.SHOW,t),i=c._getParentFromElement(this._element);if(p(i).trigger(n),!n.isDefaultPrevented()){if(!this._inNavbar&&e){if(\"undefined\"==typeof St)throw new TypeError(\"Bootstrap's dropdowns require Popper.js (https://popper.js.org/)\");var o=this._element;\"parent\"===this._config.reference?o=i:m.isElement(this._config.reference)&&(o=this._config.reference,\"undefined\"!=typeof this._config.reference.jquery&&(o=this._config.reference[0])),\"scrollParent\"!==this._config.boundary&&p(i).addClass(Wt),this._popper=new St(o,this._menu,this._getPopperConfig())}\"ontouchstart\"in document.documentElement&&0===p(i).closest(Kt).length&&p(document.body).children().on(\"mouseover\",null,p.noop),this._element.focus(),this._element.setAttribute(\"aria-expanded\",!0),p(this._menu).toggleClass(jt),p(i).toggleClass(jt).trigger(p.Event(Pt.SHOWN,t))}}},e.hide=function(){if(!this._element.disabled&&!p(this._element).hasClass(xt)&&p(this._menu).hasClass(jt)){var e={relatedTarget:this._element},t=p.Event(Pt.HIDE,e),n=c._getParentFromElement(this._element);p(n).trigger(t),t.isDefaultPrevented()||(this._popper&&this._popper.destroy(),p(this._menu).toggleClass(jt),p(n).toggleClass(jt).trigger(p.Event(Pt.HIDDEN,e)))}},e.dispose=function(){p.removeData(this._element,At),p(this._element).off(Ot),this._element=null,(this._menu=null)!==this._popper&&(this._popper.destroy(),this._popper=null)},e.update=function(){this._inNavbar=this._detectNavbar(),null!==this._popper&&this._popper.scheduleUpdate()},e._addEventListeners=function(){var t=this;p(this._element).on(Pt.CLICK,function(e){e.preventDefault(),e.stopPropagation(),t.toggle()})},e._getConfig=function(e){return e=l({},this.constructor.Default,{},p(this._element).data(),{},e),m.typeCheckConfig(It,e,this.constructor.DefaultType),e},e._getMenuElement=function(){if(!this._menu){var e=c._getParentFromElement(this._element);e&&(this._menu=e.querySelector(qt))}return this._menu},e._getPlacement=function(){var e=p(this._element.parentNode),t=zt;return e.hasClass(Ht)?(t=Vt,p(this._menu).hasClass(Mt)&&(t=Yt)):e.hasClass(Rt)?t=Gt:e.hasClass(Ft)?t=$t:p(this._menu).hasClass(Mt)&&(t=Xt),t},e._detectNavbar=function(){return 0<p(this._element).closest(\".navbar\").length},e._getOffset=function(){var t=this,e={};return\"function\"==typeof this._config.offset?e.fn=function(e){return e.offsets=l({},e.offsets,{},t._config.offset(e.offsets,t._element)||{}),e}:e.offset=this._config.offset,e},e._getPopperConfig=function(){var e={placement:this._getPlacement(),modifiers:{offset:this._getOffset(),flip:{enabled:this._config.flip},preventOverflow:{boundariesElement:this._config.boundary}}};return\"static\"===this._config.display&&(e.modifiers.applyStyle={enabled:!1}),l({},e,{},this._config.popperConfig)},c._jQueryInterface=function(t){return this.each(function(){var e=p(this).data(At);if(e||(e=new c(this,\"object\"==typeof t?t:null),p(this).data(At,e)),\"string\"==typeof t){if(\"undefined\"==typeof e[t])throw new TypeError('No method named \"'+t+'\"');e[t]()}})},c._clearMenus=function(e){if(!e||3!==e.which&&(\"keyup\"!==e.type||9===e.which))for(var t=[].slice.call(document.querySelectorAll(Ut)),n=0,i=t.length;n<i;n++){var o=c._getParentFromElement(t[n]),r=p(t[n]).data(At),s={relatedTarget:t[n]};if(e&&\"click\"===e.type&&(s.clickEvent=e),r){var a=r._menu;if(p(o).hasClass(jt)&&!(e&&(\"click\"===e.type&&/input|textarea/i.test(e.target.tagName)||\"keyup\"===e.type&&9===e.which)&&p.contains(o,e.target))){var l=p.Event(Pt.HIDE,s);p(o).trigger(l),l.isDefaultPrevented()||(\"ontouchstart\"in document.documentElement&&p(document.body).children().off(\"mouseover\",null,p.noop),t[n].setAttribute(\"aria-expanded\",\"false\"),r._popper&&r._popper.destroy(),p(a).removeClass(jt),p(o).removeClass(jt).trigger(p.Event(Pt.HIDDEN,s)))}}}},c._getParentFromElement=function(e){var t,n=m.getSelectorFromElement(e);return n&&(t=document.querySelector(n)),t||e.parentNode},c._dataApiKeydownHandler=function(e){if((/input|textarea/i.test(e.target.tagName)?!(32===e.which||27!==e.which&&(40!==e.which&&38!==e.which||p(e.target).closest(qt).length)):Lt.test(e.which))&&(e.preventDefault(),e.stopPropagation(),!this.disabled&&!p(this).hasClass(xt))){var t=c._getParentFromElement(this),n=p(t).hasClass(jt);if(n||27!==e.which)if(n&&(!n||27!==e.which&&32!==e.which)){var i=[].slice.call(t.querySelectorAll(Qt)).filter(function(e){return p(e).is(\":visible\")});if(0!==i.length){var o=i.indexOf(e.target);38===e.which&&0<o&&o--,40===e.which&&o<i.length-1&&o++,o<0&&(o=0),i[o].focus()}}else{if(27===e.which){var r=t.querySelector(Ut);p(r).trigger(\"focus\")}p(this).trigger(\"click\")}}},s(c,null,[{key:\"VERSION\",get:function(){return\"4.4.1\"}},{key:\"Default\",get:function(){return Jt}},{key:\"DefaultType\",get:function(){return Zt}}]),c}();p(document).on(Pt.KEYDOWN_DATA_API,Ut,en._dataApiKeydownHandler).on(Pt.KEYDOWN_DATA_API,qt,en._dataApiKeydownHandler).on(Pt.CLICK_DATA_API+\" \"+Pt.KEYUP_DATA_API,en._clearMenus).on(Pt.CLICK_DATA_API,Ut,function(e){e.preventDefault(),e.stopPropagation(),en._jQueryInterface.call(p(this),\"toggle\")}).on(Pt.CLICK_DATA_API,Bt,function(e){e.stopPropagation()}),p.fn[It]=en._jQueryInterface,p.fn[It].Constructor=en,p.fn[It].noConflict=function(){return p.fn[It]=kt,en._jQueryInterface};var tn=\"modal\",nn=\"bs.modal\",on=\".\"+nn,rn=p.fn[tn],sn={backdrop:!0,keyboard:!0,focus:!0,show:!0},an={backdrop:\"(boolean|string)\",keyboard:\"boolean\",focus:\"boolean\",show:\"boolean\"},ln={HIDE:\"hide\"+on,HIDE_PREVENTED:\"hidePrevented\"+on,HIDDEN:\"hidden\"+on,SHOW:\"show\"+on,SHOWN:\"shown\"+on,FOCUSIN:\"focusin\"+on,RESIZE:\"resize\"+on,CLICK_DISMISS:\"click.dismiss\"+on,KEYDOWN_DISMISS:\"keydown.dismiss\"+on,MOUSEUP_DISMISS:\"mouseup.dismiss\"+on,MOUSEDOWN_DISMISS:\"mousedown.dismiss\"+on,CLICK_DATA_API:\"click\"+on+\".data-api\"},cn=\"modal-dialog-scrollable\",hn=\"modal-scrollbar-measure\",un=\"modal-backdrop\",fn=\"modal-open\",dn=\"fade\",pn=\"show\",mn=\"modal-static\",gn=\".modal-dialog\",_n=\".modal-body\",vn='[data-toggle=\"modal\"]',yn='[data-dismiss=\"modal\"]',En=\".fixed-top, .fixed-bottom, .is-fixed, .sticky-top\",bn=\".sticky-top\",wn=function(){function o(e,t){this._config=this._getConfig(t),this._element=e,this._dialog=e.querySelector(gn),this._backdrop=null,this._isShown=!1,this._isBodyOverflowing=!1,this._ignoreBackdropClick=!1,this._isTransitioning=!1,this._scrollbarWidth=0}var e=o.prototype;return e.toggle=function(e){return this._isShown?this.hide():this.show(e)},e.show=function(e){var t=this;if(!this._isShown&&!this._isTransitioning){p(this._element).hasClass(dn)&&(this._isTransitioning=!0);var n=p.Event(ln.SHOW,{relatedTarget:e});p(this._element).trigger(n),this._isShown||n.isDefaultPrevented()||(this._isShown=!0,this._checkScrollbar(),this._setScrollbar(),this._adjustDialog(),this._setEscapeEvent(),this._setResizeEvent(),p(this._element).on(ln.CLICK_DISMISS,yn,function(e){return t.hide(e)}),p(this._dialog).on(ln.MOUSEDOWN_DISMISS,function(){p(t._element).one(ln.MOUSEUP_DISMISS,function(e){p(e.target).is(t._element)&&(t._ignoreBackdropClick=!0)})}),this._showBackdrop(function(){return t._showElement(e)}))}},e.hide=function(e){var t=this;if(e&&e.preventDefault(),this._isShown&&!this._isTransitioning){var n=p.Event(ln.HIDE);if(p(this._element).trigger(n),this._isShown&&!n.isDefaultPrevented()){this._isShown=!1;var i=p(this._element).hasClass(dn);if(i&&(this._isTransitioning=!0),this._setEscapeEvent(),this._setResizeEvent(),p(document).off(ln.FOCUSIN),p(this._element).removeClass(pn),p(this._element).off(ln.CLICK_DISMISS),p(this._dialog).off(ln.MOUSEDOWN_DISMISS),i){var o=m.getTransitionDurationFromElement(this._element);p(this._element).one(m.TRANSITION_END,function(e){return t._hideModal(e)}).emulateTransitionEnd(o)}else this._hideModal()}}},e.dispose=function(){[window,this._element,this._dialog].forEach(function(e){return p(e).off(on)}),p(document).off(ln.FOCUSIN),p.removeData(this._element,nn),this._config=null,this._element=null,this._dialog=null,this._backdrop=null,this._isShown=null,this._isBodyOverflowing=null,this._ignoreBackdropClick=null,this._isTransitioning=null,this._scrollbarWidth=null},e.handleUpdate=function(){this._adjustDialog()},e._getConfig=function(e){return e=l({},sn,{},e),m.typeCheckConfig(tn,e,an),e},e._triggerBackdropTransition=function(){var e=this;if(\"static\"===this._config.backdrop){var t=p.Event(ln.HIDE_PREVENTED);if(p(this._element).trigger(t),t.defaultPrevented)return;this._element.classList.add(mn);var n=m.getTransitionDurationFromElement(this._element);p(this._element).one(m.TRANSITION_END,function(){e._element.classList.remove(mn)}).emulateTransitionEnd(n),this._element.focus()}else this.hide()},e._showElement=function(e){var t=this,n=p(this._element).hasClass(dn),i=this._dialog?this._dialog.querySelector(_n):null;this._element.parentNode&&this._element.parentNode.nodeType===Node.ELEMENT_NODE||document.body.appendChild(this._element),this._element.style.display=\"block\",this._element.removeAttribute(\"aria-hidden\"),this._element.setAttribute(\"aria-modal\",!0),p(this._dialog).hasClass(cn)&&i?i.scrollTop=0:this._element.scrollTop=0,n&&m.reflow(this._element),p(this._element).addClass(pn),this._config.focus&&this._enforceFocus();function o(){t._config.focus&&t._element.focus(),t._isTransitioning=!1,p(t._element).trigger(r)}var r=p.Event(ln.SHOWN,{relatedTarget:e});if(n){var s=m.getTransitionDurationFromElement(this._dialog);p(this._dialog).one(m.TRANSITION_END,o).emulateTransitionEnd(s)}else o()},e._enforceFocus=function(){var t=this;p(document).off(ln.FOCUSIN).on(ln.FOCUSIN,function(e){document!==e.target&&t._element!==e.target&&0===p(t._element).has(e.target).length&&t._element.focus()})},e._setEscapeEvent=function(){var t=this;this._isShown&&this._config.keyboard?p(this._element).on(ln.KEYDOWN_DISMISS,function(e){27===e.which&&t._triggerBackdropTransition()}):this._isShown||p(this._element).off(ln.KEYDOWN_DISMISS)},e._setResizeEvent=function(){var t=this;this._isShown?p(window).on(ln.RESIZE,function(e){return t.handleUpdate(e)}):p(window).off(ln.RESIZE)},e._hideModal=function(){var e=this;this._element.style.display=\"none\",this._element.setAttribute(\"aria-hidden\",!0),this._element.removeAttribute(\"aria-modal\"),this._isTransitioning=!1,this._showBackdrop(function(){p(document.body).removeClass(fn),e._resetAdjustments(),e._resetScrollbar(),p(e._element).trigger(ln.HIDDEN)})},e._removeBackdrop=function(){this._backdrop&&(p(this._backdrop).remove(),this._backdrop=null)},e._showBackdrop=function(e){var t=this,n=p(this._element).hasClass(dn)?dn:\"\";if(this._isShown&&this._config.backdrop){if(this._backdrop=document.createElement(\"div\"),this._backdrop.className=un,n&&this._backdrop.classList.add(n),p(this._backdrop).appendTo(document.body),p(this._element).on(ln.CLICK_DISMISS,function(e){t._ignoreBackdropClick?t._ignoreBackdropClick=!1:e.target===e.currentTarget&&t._triggerBackdropTransition()}),n&&m.reflow(this._backdrop),p(this._backdrop).addClass(pn),!e)return;if(!n)return void e();var i=m.getTransitionDurationFromElement(this._backdrop);p(this._backdrop).one(m.TRANSITION_END,e).emulateTransitionEnd(i)}else if(!this._isShown&&this._backdrop){p(this._backdrop).removeClass(pn);var o=function(){t._removeBackdrop(),e&&e()};if(p(this._element).hasClass(dn)){var r=m.getTransitionDurationFromElement(this._backdrop);p(this._backdrop).one(m.TRANSITION_END,o).emulateTransitionEnd(r)}else o()}else e&&e()},e._adjustDialog=function(){var e=this._element.scrollHeight>document.documentElement.clientHeight;!this._isBodyOverflowing&&e&&(this._element.style.paddingLeft=this._scrollbarWidth+\"px\"),this._isBodyOverflowing&&!e&&(this._element.style.paddingRight=this._scrollbarWidth+\"px\")},e._resetAdjustments=function(){this._element.style.paddingLeft=\"\",this._element.style.paddingRight=\"\"},e._checkScrollbar=function(){var e=document.body.getBoundingClientRect();this._isBodyOverflowing=e.left+e.right<window.innerWidth,this._scrollbarWidth=this._getScrollbarWidth()},e._setScrollbar=function(){var o=this;if(this._isBodyOverflowing){var e=[].slice.call(document.querySelectorAll(En)),t=[].slice.call(document.querySelectorAll(bn));p(e).each(function(e,t){var n=t.style.paddingRight,i=p(t).css(\"padding-right\");p(t).data(\"padding-right\",n).css(\"padding-right\",parseFloat(i)+o._scrollbarWidth+\"px\")}),p(t).each(function(e,t){var n=t.style.marginRight,i=p(t).css(\"margin-right\");p(t).data(\"margin-right\",n).css(\"margin-right\",parseFloat(i)-o._scrollbarWidth+\"px\")});var n=document.body.style.paddingRight,i=p(document.body).css(\"padding-right\");p(document.body).data(\"padding-right\",n).css(\"padding-right\",parseFloat(i)+this._scrollbarWidth+\"px\")}p(document.body).addClass(fn)},e._resetScrollbar=function(){var e=[].slice.call(document.querySelectorAll(En));p(e).each(function(e,t){var n=p(t).data(\"padding-right\");p(t).removeData(\"padding-right\"),t.style.paddingRight=n||\"\"});var t=[].slice.call(document.querySelectorAll(\"\"+bn));p(t).each(function(e,t){var n=p(t).data(\"margin-right\");\"undefined\"!=typeof n&&p(t).css(\"margin-right\",n).removeData(\"margin-right\")});var n=p(document.body).data(\"padding-right\");p(document.body).removeData(\"padding-right\"),document.body.style.paddingRight=n||\"\"},e._getScrollbarWidth=function(){var e=document.createElement(\"div\");e.className=hn,document.body.appendChild(e);var t=e.getBoundingClientRect().width-e.clientWidth;return document.body.removeChild(e),t},o._jQueryInterface=function(n,i){return this.each(function(){var e=p(this).data(nn),t=l({},sn,{},p(this).data(),{},\"object\"==typeof n&&n?n:{});if(e||(e=new o(this,t),p(this).data(nn,e)),\"string\"==typeof n){if(\"undefined\"==typeof e[n])throw new TypeError('No method named \"'+n+'\"');e[n](i)}else t.show&&e.show(i)})},s(o,null,[{key:\"VERSION\",get:function(){return\"4.4.1\"}},{key:\"Default\",get:function(){return sn}}]),o}();p(document).on(ln.CLICK_DATA_API,vn,function(e){var t,n=this,i=m.getSelectorFromElement(this);i&&(t=document.querySelector(i));var o=p(t).data(nn)?\"toggle\":l({},p(t).data(),{},p(this).data());\"A\"!==this.tagName&&\"AREA\"!==this.tagName||e.preventDefault();var r=p(t).one(ln.SHOW,function(e){e.isDefaultPrevented()||r.one(ln.HIDDEN,function(){p(n).is(\":visible\")&&n.focus()})});wn._jQueryInterface.call(p(t),o,this)}),p.fn[tn]=wn._jQueryInterface,p.fn[tn].Constructor=wn,p.fn[tn].noConflict=function(){return p.fn[tn]=rn,wn._jQueryInterface};var Tn=[\"background\",\"cite\",\"href\",\"itemtype\",\"longdesc\",\"poster\",\"src\",\"xlink:href\"],Cn={\"*\":[\"class\",\"dir\",\"id\",\"lang\",\"role\",/^aria-[\\w-]*$/i],a:[\"target\",\"href\",\"title\",\"rel\"],area:[],b:[],br:[],col:[],code:[],div:[],em:[],hr:[],h1:[],h2:[],h3:[],h4:[],h5:[],h6:[],i:[],img:[\"src\",\"alt\",\"title\",\"width\",\"height\"],li:[],ol:[],p:[],pre:[],s:[],small:[],span:[],sub:[],sup:[],strong:[],u:[],ul:[]},Sn=/^(?:(?:https?|mailto|ftp|tel|file):|[^&:/?#]*(?:[/?#]|$))/gi,Dn=/^data:(?:image\\/(?:bmp|gif|jpeg|jpg|png|tiff|webp)|video\\/(?:mpeg|mp4|ogg|webm)|audio\\/(?:mp3|oga|ogg|opus));base64,[a-z0-9+/]+=*$/i;function In(e,r,t){if(0===e.length)return e;if(t&&\"function\"==typeof t)return t(e);for(var n=(new window.DOMParser).parseFromString(e,\"text/html\"),s=Object.keys(r),a=[].slice.call(n.body.querySelectorAll(\"*\")),i=function(e){var t=a[e],n=t.nodeName.toLowerCase();if(-1===s.indexOf(t.nodeName.toLowerCase()))return t.parentNode.removeChild(t),\"continue\";var i=[].slice.call(t.attributes),o=[].concat(r[\"*\"]||[],r[n]||[]);i.forEach(function(e){!function(e,t){var n=e.nodeName.toLowerCase();if(-1!==t.indexOf(n))return-1===Tn.indexOf(n)||Boolean(e.nodeValue.match(Sn)||e.nodeValue.match(Dn));for(var i=t.filter(function(e){return e instanceof RegExp}),o=0,r=i.length;o<r;o++)if(n.match(i[o]))return!0;return!1}(e,o)&&t.removeAttribute(e.nodeName)})},o=0,l=a.length;o<l;o++)i(o);return n.body.innerHTML}var An=\"tooltip\",On=\"bs.tooltip\",Nn=\".\"+On,kn=p.fn[An],Ln=\"bs-tooltip\",Pn=new RegExp(\"(^|\\\\s)\"+Ln+\"\\\\S+\",\"g\"),xn=[\"sanitize\",\"whiteList\",\"sanitizeFn\"],jn={animation:\"boolean\",template:\"string\",title:\"(string|element|function)\",trigger:\"string\",delay:\"(number|object)\",html:\"boolean\",selector:\"(string|boolean)\",placement:\"(string|function)\",offset:\"(number|string|function)\",container:\"(string|element|boolean)\",fallbackPlacement:\"(string|array)\",boundary:\"(string|element)\",sanitize:\"boolean\",sanitizeFn:\"(null|function)\",whiteList:\"object\",popperConfig:\"(null|object)\"},Hn={AUTO:\"auto\",TOP:\"top\",RIGHT:\"right\",BOTTOM:\"bottom\",LEFT:\"left\"},Rn={animation:!0,template:'<div class=\"tooltip\" role=\"tooltip\"><div class=\"arrow\"></div><div class=\"tooltip-inner\"></div></div>',trigger:\"hover focus\",title:\"\",delay:0,html:!1,selector:!1,placement:\"top\",offset:0,container:!1,fallbackPlacement:\"flip\",boundary:\"scrollParent\",sanitize:!0,sanitizeFn:null,whiteList:Cn,popperConfig:null},Fn=\"show\",Mn=\"out\",Wn={HIDE:\"hide\"+Nn,HIDDEN:\"hidden\"+Nn,SHOW:\"show\"+Nn,SHOWN:\"shown\"+Nn,INSERTED:\"inserted\"+Nn,CLICK:\"click\"+Nn,FOCUSIN:\"focusin\"+Nn,FOCUSOUT:\"focusout\"+Nn,MOUSEENTER:\"mouseenter\"+Nn,MOUSELEAVE:\"mouseleave\"+Nn},Un=\"fade\",Bn=\"show\",qn=\".tooltip-inner\",Kn=\".arrow\",Qn=\"hover\",Vn=\"focus\",Yn=\"click\",zn=\"manual\",Xn=function(){function i(e,t){if(\"undefined\"==typeof St)throw new TypeError(\"Bootstrap's tooltips require Popper.js (https://popper.js.org/)\");this._isEnabled=!0,this._timeout=0,this._hoverState=\"\",this._activeTrigger={},this._popper=null,this.element=e,this.config=this._getConfig(t),this.tip=null,this._setListeners()}var e=i.prototype;return e.enable=function(){this._isEnabled=!0},e.disable=function(){this._isEnabled=!1},e.toggleEnabled=function(){this._isEnabled=!this._isEnabled},e.toggle=function(e){if(this._isEnabled)if(e){var t=this.constructor.DATA_KEY,n=p(e.currentTarget).data(t);n||(n=new this.constructor(e.currentTarget,this._getDelegateConfig()),p(e.currentTarget).data(t,n)),n._activeTrigger.click=!n._activeTrigger.click,n._isWithActiveTrigger()?n._enter(null,n):n._leave(null,n)}else{if(p(this.getTipElement()).hasClass(Bn))return void this._leave(null,this);this._enter(null,this)}},e.dispose=function(){clearTimeout(this._timeout),p.removeData(this.element,this.constructor.DATA_KEY),p(this.element).off(this.constructor.EVENT_KEY),p(this.element).closest(\".modal\").off(\"hide.bs.modal\",this._hideModalHandler),this.tip&&p(this.tip).remove(),this._isEnabled=null,this._timeout=null,this._hoverState=null,this._activeTrigger=null,this._popper&&this._popper.destroy(),this._popper=null,this.element=null,this.config=null,this.tip=null},e.show=function(){var t=this;if(\"none\"===p(this.element).css(\"display\"))throw new Error(\"Please use show on visible elements\");var e=p.Event(this.constructor.Event.SHOW);if(this.isWithContent()&&this._isEnabled){p(this.element).trigger(e);var n=m.findShadowRoot(this.element),i=p.contains(null!==n?n:this.element.ownerDocument.documentElement,this.element);if(e.isDefaultPrevented()||!i)return;var o=this.getTipElement(),r=m.getUID(this.constructor.NAME);o.setAttribute(\"id\",r),this.element.setAttribute(\"aria-describedby\",r),this.setContent(),this.config.animation&&p(o).addClass(Un);var s=\"function\"==typeof this.config.placement?this.config.placement.call(this,o,this.element):this.config.placement,a=this._getAttachment(s);this.addAttachmentClass(a);var l=this._getContainer();p(o).data(this.constructor.DATA_KEY,this),p.contains(this.element.ownerDocument.documentElement,this.tip)||p(o).appendTo(l),p(this.element).trigger(this.constructor.Event.INSERTED),this._popper=new St(this.element,o,this._getPopperConfig(a)),p(o).addClass(Bn),\"ontouchstart\"in document.documentElement&&p(document.body).children().on(\"mouseover\",null,p.noop);var c=function(){t.config.animation&&t._fixTransition();var e=t._hoverState;t._hoverState=null,p(t.element).trigger(t.constructor.Event.SHOWN),e===Mn&&t._leave(null,t)};if(p(this.tip).hasClass(Un)){var h=m.getTransitionDurationFromElement(this.tip);p(this.tip).one(m.TRANSITION_END,c).emulateTransitionEnd(h)}else c()}},e.hide=function(e){function t(){n._hoverState!==Fn&&i.parentNode&&i.parentNode.removeChild(i),n._cleanTipClass(),n.element.removeAttribute(\"aria-describedby\"),p(n.element).trigger(n.constructor.Event.HIDDEN),null!==n._popper&&n._popper.destroy(),e&&e()}var n=this,i=this.getTipElement(),o=p.Event(this.constructor.Event.HIDE);if(p(this.element).trigger(o),!o.isDefaultPrevented()){if(p(i).removeClass(Bn),\"ontouchstart\"in document.documentElement&&p(document.body).children().off(\"mouseover\",null,p.noop),this._activeTrigger[Yn]=!1,this._activeTrigger[Vn]=!1,this._activeTrigger[Qn]=!1,p(this.tip).hasClass(Un)){var r=m.getTransitionDurationFromElement(i);p(i).one(m.TRANSITION_END,t).emulateTransitionEnd(r)}else t();this._hoverState=\"\"}},e.update=function(){null!==this._popper&&this._popper.scheduleUpdate()},e.isWithContent=function(){return Boolean(this.getTitle())},e.addAttachmentClass=function(e){p(this.getTipElement()).addClass(Ln+\"-\"+e)},e.getTipElement=function(){return this.tip=this.tip||p(this.config.template)[0],this.tip},e.setContent=function(){var e=this.getTipElement();this.setElementContent(p(e.querySelectorAll(qn)),this.getTitle()),p(e).removeClass(Un+\" \"+Bn)},e.setElementContent=function(e,t){\"object\"!=typeof t||!t.nodeType&&!t.jquery?this.config.html?(this.config.sanitize&&(t=In(t,this.config.whiteList,this.config.sanitizeFn)),e.html(t)):e.text(t):this.config.html?p(t).parent().is(e)||e.empty().append(t):e.text(p(t).text())},e.getTitle=function(){var e=this.element.getAttribute(\"data-original-title\");return e=e||(\"function\"==typeof this.config.title?this.config.title.call(this.element):this.config.title)},e._getPopperConfig=function(e){var t=this;return l({},{placement:e,modifiers:{offset:this._getOffset(),flip:{behavior:this.config.fallbackPlacement},arrow:{element:Kn},preventOverflow:{boundariesElement:this.config.boundary}},onCreate:function(e){e.originalPlacement!==e.placement&&t._handlePopperPlacementChange(e)},onUpdate:function(e){return t._handlePopperPlacementChange(e)}},{},this.config.popperConfig)},e._getOffset=function(){var t=this,e={};return\"function\"==typeof this.config.offset?e.fn=function(e){return e.offsets=l({},e.offsets,{},t.config.offset(e.offsets,t.element)||{}),e}:e.offset=this.config.offset,e},e._getContainer=function(){return!1===this.config.container?document.body:m.isElement(this.config.container)?p(this.config.container):p(document).find(this.config.container)},e._getAttachment=function(e){return Hn[e.toUpperCase()]},e._setListeners=function(){var i=this;this.config.trigger.split(\" \").forEach(function(e){if(\"click\"===e)p(i.element).on(i.constructor.Event.CLICK,i.config.selector,function(e){return i.toggle(e)});else if(e!==zn){var t=e===Qn?i.constructor.Event.MOUSEENTER:i.constructor.Event.FOCUSIN,n=e===Qn?i.constructor.Event.MOUSELEAVE:i.constructor.Event.FOCUSOUT;p(i.element).on(t,i.config.selector,function(e){return i._enter(e)}).on(n,i.config.selector,function(e){return i._leave(e)})}}),this._hideModalHandler=function(){i.element&&i.hide()},p(this.element).closest(\".modal\").on(\"hide.bs.modal\",this._hideModalHandler),this.config.selector?this.config=l({},this.config,{trigger:\"manual\",selector:\"\"}):this._fixTitle()},e._fixTitle=function(){var e=typeof this.element.getAttribute(\"data-original-title\");!this.element.getAttribute(\"title\")&&\"string\"==e||(this.element.setAttribute(\"data-original-title\",this.element.getAttribute(\"title\")||\"\"),this.element.setAttribute(\"title\",\"\"))},e._enter=function(e,t){var n=this.constructor.DATA_KEY;(t=t||p(e.currentTarget).data(n))||(t=new this.constructor(e.currentTarget,this._getDelegateConfig()),p(e.currentTarget).data(n,t)),e&&(t._activeTrigger[\"focusin\"===e.type?Vn:Qn]=!0),p(t.getTipElement()).hasClass(Bn)||t._hoverState===Fn?t._hoverState=Fn:(clearTimeout(t._timeout),t._hoverState=Fn,t.config.delay&&t.config.delay.show?t._timeout=setTimeout(function(){t._hoverState===Fn&&t.show()},t.config.delay.show):t.show())},e._leave=function(e,t){var n=this.constructor.DATA_KEY;(t=t||p(e.currentTarget).data(n))||(t=new this.constructor(e.currentTarget,this._getDelegateConfig()),p(e.currentTarget).data(n,t)),e&&(t._activeTrigger[\"focusout\"===e.type?Vn:Qn]=!1),t._isWithActiveTrigger()||(clearTimeout(t._timeout),t._hoverState=Mn,t.config.delay&&t.config.delay.hide?t._timeout=setTimeout(function(){t._hoverState===Mn&&t.hide()},t.config.delay.hide):t.hide())},e._isWithActiveTrigger=function(){for(var e in this._activeTrigger)if(this._activeTrigger[e])return!0;return!1},e._getConfig=function(e){var t=p(this.element).data();return Object.keys(t).forEach(function(e){-1!==xn.indexOf(e)&&delete t[e]}),\"number\"==typeof(e=l({},this.constructor.Default,{},t,{},\"object\"==typeof e&&e?e:{})).delay&&(e.delay={show:e.delay,hide:e.delay}),\"number\"==typeof e.title&&(e.title=e.title.toString()),\"number\"==typeof e.content&&(e.content=e.content.toString()),m.typeCheckConfig(An,e,this.constructor.DefaultType),e.sanitize&&(e.template=In(e.template,e.whiteList,e.sanitizeFn)),e},e._getDelegateConfig=function(){var e={};if(this.config)for(var t in this.config)this.constructor.Default[t]!==this.config[t]&&(e[t]=this.config[t]);return e},e._cleanTipClass=function(){var e=p(this.getTipElement()),t=e.attr(\"class\").match(Pn);null!==t&&t.length&&e.removeClass(t.join(\"\"))},e._handlePopperPlacementChange=function(e){var t=e.instance;this.tip=t.popper,this._cleanTipClass(),this.addAttachmentClass(this._getAttachment(e.placement))},e._fixTransition=function(){var e=this.getTipElement(),t=this.config.animation;null===e.getAttribute(\"x-placement\")&&(p(e).removeClass(Un),this.config.animation=!1,this.hide(),this.show(),this.config.animation=t)},i._jQueryInterface=function(n){return this.each(function(){var e=p(this).data(On),t=\"object\"==typeof n&&n;if((e||!/dispose|hide/.test(n))&&(e||(e=new i(this,t),p(this).data(On,e)),\"string\"==typeof n)){if(\"undefined\"==typeof e[n])throw new TypeError('No method named \"'+n+'\"');e[n]()}})},s(i,null,[{key:\"VERSION\",get:function(){return\"4.4.1\"}},{key:\"Default\",get:function(){return Rn}},{key:\"NAME\",get:function(){return An}},{key:\"DATA_KEY\",get:function(){return On}},{key:\"Event\",get:function(){return Wn}},{key:\"EVENT_KEY\",get:function(){return Nn}},{key:\"DefaultType\",get:function(){return jn}}]),i}();p.fn[An]=Xn._jQueryInterface,p.fn[An].Constructor=Xn,p.fn[An].noConflict=function(){return p.fn[An]=kn,Xn._jQueryInterface};var Gn=\"popover\",$n=\"bs.popover\",Jn=\".\"+$n,Zn=p.fn[Gn],ei=\"bs-popover\",ti=new RegExp(\"(^|\\\\s)\"+ei+\"\\\\S+\",\"g\"),ni=l({},Xn.Default,{placement:\"right\",trigger:\"click\",content:\"\",template:'<div class=\"popover\" role=\"tooltip\"><div class=\"arrow\"></div><h3 class=\"popover-header\"></h3><div class=\"popover-body\"></div></div>'}),ii=l({},Xn.DefaultType,{content:\"(string|element|function)\"}),oi=\"fade\",ri=\"show\",si=\".popover-header\",ai=\".popover-body\",li={HIDE:\"hide\"+Jn,HIDDEN:\"hidden\"+Jn,SHOW:\"show\"+Jn,SHOWN:\"shown\"+Jn,INSERTED:\"inserted\"+Jn,CLICK:\"click\"+Jn,FOCUSIN:\"focusin\"+Jn,FOCUSOUT:\"focusout\"+Jn,MOUSEENTER:\"mouseenter\"+Jn,MOUSELEAVE:\"mouseleave\"+Jn},ci=function(e){function i(){return e.apply(this,arguments)||this}!function(e,t){e.prototype=Object.create(t.prototype),(e.prototype.constructor=e).__proto__=t}(i,e);var t=i.prototype;return t.isWithContent=function(){return this.getTitle()||this._getContent()},t.addAttachmentClass=function(e){p(this.getTipElement()).addClass(ei+\"-\"+e)},t.getTipElement=function(){return this.tip=this.tip||p(this.config.template)[0],this.tip},t.setContent=function(){var e=p(this.getTipElement());this.setElementContent(e.find(si),this.getTitle());var t=this._getContent();\"function\"==typeof t&&(t=t.call(this.element)),this.setElementContent(e.find(ai),t),e.removeClass(oi+\" \"+ri)},t._getContent=function(){return this.element.getAttribute(\"data-content\")||this.config.content},t._cleanTipClass=function(){var e=p(this.getTipElement()),t=e.attr(\"class\").match(ti);null!==t&&0<t.length&&e.removeClass(t.join(\"\"))},i._jQueryInterface=function(n){return this.each(function(){var e=p(this).data($n),t=\"object\"==typeof n?n:null;if((e||!/dispose|hide/.test(n))&&(e||(e=new i(this,t),p(this).data($n,e)),\"string\"==typeof n)){if(\"undefined\"==typeof e[n])throw new TypeError('No method named \"'+n+'\"');e[n]()}})},s(i,null,[{key:\"VERSION\",get:function(){return\"4.4.1\"}},{key:\"Default\",get:function(){return ni}},{key:\"NAME\",get:function(){return Gn}},{key:\"DATA_KEY\",get:function(){return $n}},{key:\"Event\",get:function(){return li}},{key:\"EVENT_KEY\",get:function(){return Jn}},{key:\"DefaultType\",get:function(){return ii}}]),i}(Xn);p.fn[Gn]=ci._jQueryInterface,p.fn[Gn].Constructor=ci,p.fn[Gn].noConflict=function(){return p.fn[Gn]=Zn,ci._jQueryInterface};var hi=\"scrollspy\",ui=\"bs.scrollspy\",fi=\".\"+ui,di=p.fn[hi],pi={offset:10,method:\"auto\",target:\"\"},mi={offset:\"number\",method:\"string\",target:\"(string|element)\"},gi={ACTIVATE:\"activate\"+fi,SCROLL:\"scroll\"+fi,LOAD_DATA_API:\"load\"+fi+\".data-api\"},_i=\"dropdown-item\",vi=\"active\",yi='[data-spy=\"scroll\"]',Ei=\".nav, .list-group\",bi=\".nav-link\",wi=\".nav-item\",Ti=\".list-group-item\",Ci=\".dropdown\",Si=\".dropdown-item\",Di=\".dropdown-toggle\",Ii=\"offset\",Ai=\"position\",Oi=function(){function n(e,t){var n=this;this._element=e,this._scrollElement=\"BODY\"===e.tagName?window:e,this._config=this._getConfig(t),this._selector=this._config.target+\" \"+bi+\",\"+this._config.target+\" \"+Ti+\",\"+this._config.target+\" \"+Si,this._offsets=[],this._targets=[],this._activeTarget=null,this._scrollHeight=0,p(this._scrollElement).on(gi.SCROLL,function(e){return n._process(e)}),this.refresh(),this._process()}var e=n.prototype;return e.refresh=function(){var t=this,e=this._scrollElement===this._scrollElement.window?Ii:Ai,o=\"auto\"===this._config.method?e:this._config.method,r=o===Ai?this._getScrollTop():0;this._offsets=[],this._targets=[],this._scrollHeight=this._getScrollHeight(),[].slice.call(document.querySelectorAll(this._selector)).map(function(e){var t,n=m.getSelectorFromElement(e);if(n&&(t=document.querySelector(n)),t){var i=t.getBoundingClientRect();if(i.width||i.height)return[p(t)[o]().top+r,n]}return null}).filter(function(e){return e}).sort(function(e,t){return e[0]-t[0]}).forEach(function(e){t._offsets.push(e[0]),t._targets.push(e[1])})},e.dispose=function(){p.removeData(this._element,ui),p(this._scrollElement).off(fi),this._element=null,this._scrollElement=null,this._config=null,this._selector=null,this._offsets=null,this._targets=null,this._activeTarget=null,this._scrollHeight=null},e._getConfig=function(e){if(\"string\"!=typeof(e=l({},pi,{},\"object\"==typeof e&&e?e:{})).target){var t=p(e.target).attr(\"id\");t||(t=m.getUID(hi),p(e.target).attr(\"id\",t)),e.target=\"#\"+t}return m.typeCheckConfig(hi,e,mi),e},e._getScrollTop=function(){return this._scrollElement===window?this._scrollElement.pageYOffset:this._scrollElement.scrollTop},e._getScrollHeight=function(){return this._scrollElement.scrollHeight||Math.max(document.body.scrollHeight,document.documentElement.scrollHeight)},e._getOffsetHeight=function(){return this._scrollElement===window?window.innerHeight:this._scrollElement.getBoundingClientRect().height},e._process=function(){var e=this._getScrollTop()+this._config.offset,t=this._getScrollHeight(),n=this._config.offset+t-this._getOffsetHeight();if(this._scrollHeight!==t&&this.refresh(),n<=e){var i=this._targets[this._targets.length-1];this._activeTarget!==i&&this._activate(i)}else{if(this._activeTarget&&e<this._offsets[0]&&0<this._offsets[0])return this._activeTarget=null,void this._clear();for(var o=this._offsets.length;o--;){this._activeTarget!==this._targets[o]&&e>=this._offsets[o]&&(\"undefined\"==typeof this._offsets[o+1]||e<this._offsets[o+1])&&this._activate(this._targets[o])}}},e._activate=function(t){this._activeTarget=t,this._clear();var e=this._selector.split(\",\").map(function(e){return e+'[data-target=\"'+t+'\"],'+e+'[href=\"'+t+'\"]'}),n=p([].slice.call(document.querySelectorAll(e.join(\",\"))));n.hasClass(_i)?(n.closest(Ci).find(Di).addClass(vi),n.addClass(vi)):(n.addClass(vi),n.parents(Ei).prev(bi+\", \"+Ti).addClass(vi),n.parents(Ei).prev(wi).children(bi).addClass(vi)),p(this._scrollElement).trigger(gi.ACTIVATE,{relatedTarget:t})},e._clear=function(){[].slice.call(document.querySelectorAll(this._selector)).filter(function(e){return e.classList.contains(vi)}).forEach(function(e){return e.classList.remove(vi)})},n._jQueryInterface=function(t){return this.each(function(){var e=p(this).data(ui);if(e||(e=new n(this,\"object\"==typeof t&&t),p(this).data(ui,e)),\"string\"==typeof t){if(\"undefined\"==typeof e[t])throw new TypeError('No method named \"'+t+'\"');e[t]()}})},s(n,null,[{key:\"VERSION\",get:function(){return\"4.4.1\"}},{key:\"Default\",get:function(){return pi}}]),n}();p(window).on(gi.LOAD_DATA_API,function(){for(var e=[].slice.call(document.querySelectorAll(yi)),t=e.length;t--;){var n=p(e[t]);Oi._jQueryInterface.call(n,n.data())}}),p.fn[hi]=Oi._jQueryInterface,p.fn[hi].Constructor=Oi,p.fn[hi].noConflict=function(){return p.fn[hi]=di,Oi._jQueryInterface};var Ni=\"bs.tab\",ki=\".\"+Ni,Li=p.fn.tab,Pi={HIDE:\"hide\"+ki,HIDDEN:\"hidden\"+ki,SHOW:\"show\"+ki,SHOWN:\"shown\"+ki,CLICK_DATA_API:\"click\"+ki+\".data-api\"},xi=\"dropdown-menu\",ji=\"active\",Hi=\"disabled\",Ri=\"fade\",Fi=\"show\",Mi=\".dropdown\",Wi=\".nav, .list-group\",Ui=\".active\",Bi=\"> li > .active\",qi='[data-toggle=\"tab\"], [data-toggle=\"pill\"], [data-toggle=\"list\"]',Ki=\".dropdown-toggle\",Qi=\"> .dropdown-menu .active\",Vi=function(){function i(e){this._element=e}var e=i.prototype;return e.show=function(){var n=this;if(!(this._element.parentNode&&this._element.parentNode.nodeType===Node.ELEMENT_NODE&&p(this._element).hasClass(ji)||p(this._element).hasClass(Hi))){var e,i,t=p(this._element).closest(Wi)[0],o=m.getSelectorFromElement(this._element);if(t){var r=\"UL\"===t.nodeName||\"OL\"===t.nodeName?Bi:Ui;i=(i=p.makeArray(p(t).find(r)))[i.length-1]}var s=p.Event(Pi.HIDE,{relatedTarget:this._element}),a=p.Event(Pi.SHOW,{relatedTarget:i});if(i&&p(i).trigger(s),p(this._element).trigger(a),!a.isDefaultPrevented()&&!s.isDefaultPrevented()){o&&(e=document.querySelector(o)),this._activate(this._element,t);var l=function(){var e=p.Event(Pi.HIDDEN,{relatedTarget:n._element}),t=p.Event(Pi.SHOWN,{relatedTarget:i});p(i).trigger(e),p(n._element).trigger(t)};e?this._activate(e,e.parentNode,l):l()}}},e.dispose=function(){p.removeData(this._element,Ni),this._element=null},e._activate=function(e,t,n){function i(){return o._transitionComplete(e,r,n)}var o=this,r=(!t||\"UL\"!==t.nodeName&&\"OL\"!==t.nodeName?p(t).children(Ui):p(t).find(Bi))[0],s=n&&r&&p(r).hasClass(Ri);if(r&&s){var a=m.getTransitionDurationFromElement(r);p(r).removeClass(Fi).one(m.TRANSITION_END,i).emulateTransitionEnd(a)}else i()},e._transitionComplete=function(e,t,n){if(t){p(t).removeClass(ji);var i=p(t.parentNode).find(Qi)[0];i&&p(i).removeClass(ji),\"tab\"===t.getAttribute(\"role\")&&t.setAttribute(\"aria-selected\",!1)}if(p(e).addClass(ji),\"tab\"===e.getAttribute(\"role\")&&e.setAttribute(\"aria-selected\",!0),m.reflow(e),e.classList.contains(Ri)&&e.classList.add(Fi),e.parentNode&&p(e.parentNode).hasClass(xi)){var o=p(e).closest(Mi)[0];if(o){var r=[].slice.call(o.querySelectorAll(Ki));p(r).addClass(ji)}e.setAttribute(\"aria-expanded\",!0)}n&&n()},i._jQueryInterface=function(n){return this.each(function(){var e=p(this),t=e.data(Ni);if(t||(t=new i(this),e.data(Ni,t)),\"string\"==typeof n){if(\"undefined\"==typeof t[n])throw new TypeError('No method named \"'+n+'\"');t[n]()}})},s(i,null,[{key:\"VERSION\",get:function(){return\"4.4.1\"}}]),i}();p(document).on(Pi.CLICK_DATA_API,qi,function(e){e.preventDefault(),Vi._jQueryInterface.call(p(this),\"show\")}),p.fn.tab=Vi._jQueryInterface,p.fn.tab.Constructor=Vi,p.fn.tab.noConflict=function(){return p.fn.tab=Li,Vi._jQueryInterface};var Yi=\"toast\",zi=\"bs.toast\",Xi=\".\"+zi,Gi=p.fn[Yi],$i={CLICK_DISMISS:\"click.dismiss\"+Xi,HIDE:\"hide\"+Xi,HIDDEN:\"hidden\"+Xi,SHOW:\"show\"+Xi,SHOWN:\"shown\"+Xi},Ji=\"fade\",Zi=\"hide\",eo=\"show\",to=\"showing\",no={animation:\"boolean\",autohide:\"boolean\",delay:\"number\"},io={animation:!0,autohide:!0,delay:500},oo='[data-dismiss=\"toast\"]',ro=function(){function i(e,t){this._element=e,this._config=this._getConfig(t),this._timeout=null,this._setListeners()}var e=i.prototype;return e.show=function(){var e=this,t=p.Event($i.SHOW);if(p(this._element).trigger(t),!t.isDefaultPrevented()){this._config.animation&&this._element.classList.add(Ji);var n=function(){e._element.classList.remove(to),e._element.classList.add(eo),p(e._element).trigger($i.SHOWN),e._config.autohide&&(e._timeout=setTimeout(function(){e.hide()},e._config.delay))};if(this._element.classList.remove(Zi),m.reflow(this._element),this._element.classList.add(to),this._config.animation){var i=m.getTransitionDurationFromElement(this._element);p(this._element).one(m.TRANSITION_END,n).emulateTransitionEnd(i)}else n()}},e.hide=function(){if(this._element.classList.contains(eo)){var e=p.Event($i.HIDE);p(this._element).trigger(e),e.isDefaultPrevented()||this._close()}},e.dispose=function(){clearTimeout(this._timeout),this._timeout=null,this._element.classList.contains(eo)&&this._element.classList.remove(eo),p(this._element).off($i.CLICK_DISMISS),p.removeData(this._element,zi),this._element=null,this._config=null},e._getConfig=function(e){return e=l({},io,{},p(this._element).data(),{},\"object\"==typeof e&&e?e:{}),m.typeCheckConfig(Yi,e,this.constructor.DefaultType),e},e._setListeners=function(){var e=this;p(this._element).on($i.CLICK_DISMISS,oo,function(){return e.hide()})},e._close=function(){function e(){t._element.classList.add(Zi),p(t._element).trigger($i.HIDDEN)}var t=this;if(this._element.classList.remove(eo),this._config.animation){var n=m.getTransitionDurationFromElement(this._element);p(this._element).one(m.TRANSITION_END,e).emulateTransitionEnd(n)}else e()},i._jQueryInterface=function(n){return this.each(function(){var e=p(this),t=e.data(zi);if(t||(t=new i(this,\"object\"==typeof n&&n),e.data(zi,t)),\"string\"==typeof n){if(\"undefined\"==typeof t[n])throw new TypeError('No method named \"'+n+'\"');t[n](this)}})},s(i,null,[{key:\"VERSION\",get:function(){return\"4.4.1\"}},{key:\"DefaultType\",get:function(){return no}},{key:\"Default\",get:function(){return io}}]),i}();p.fn[Yi]=ro._jQueryInterface,p.fn[Yi].Constructor=ro,p.fn[Yi].noConflict=function(){return p.fn[Yi]=Gi,ro._jQueryInterface},e.Alert=_,e.Button=x,e.Carousel=he,e.Collapse=De,e.Dropdown=en,e.Modal=wn,e.Popover=ci,e.Scrollspy=Oi,e.Tab=Vi,e.Toast=ro,e.Tooltip=Xn,e.Util=m,Object.defineProperty(e,\"__esModule\",{value:!0})});\n//# sourceMappingURL=bootstrap.js.map"
  },
  {
    "path": "examples/django_example/django_example/static/js/jquery.js",
    "content": "/*!\n * jQuery JavaScript Library v2.0.3\n * http://jquery.com/\n *\n * Includes Sizzle.js\n * http://sizzlejs.com/\n *\n * Copyright 2005, 2013 jQuery Foundation, Inc. and other contributors\n * Released under the MIT license\n * http://jquery.org/license\n *\n * Date: 2013-07-03T13:30Z\n */\n(function( window, undefined ) {\n\n// Can't do this because several apps including ASP.NET trace\n// the stack via arguments.caller.callee and Firefox dies if\n// you try to trace through \"use strict\" call chains. (#13335)\n// Support: Firefox 18+\n//\"use strict\";\nvar\n  // A central reference to the root jQuery(document)\n  rootjQuery,\n\n  // The deferred used on DOM ready\n  readyList,\n\n  // Support: IE9\n  // For `typeof xmlNode.method` instead of `xmlNode.method !== undefined`\n  core_strundefined = typeof undefined,\n\n  // Use the correct document accordingly with window argument (sandbox)\n  location = window.location,\n  document = window.document,\n  docElem = document.documentElement,\n\n  // Map over jQuery in case of overwrite\n  _jQuery = window.jQuery,\n\n  // Map over the $ in case of overwrite\n  _$ = window.$,\n\n  // [[Class]] -> type pairs\n  class2type = {},\n\n  // List of deleted data cache ids, so we can reuse them\n  core_deletedIds = [],\n\n  core_version = \"2.0.3\",\n\n  // Save a reference to some core methods\n  core_concat = core_deletedIds.concat,\n  core_push = core_deletedIds.push,\n  core_slice = core_deletedIds.slice,\n  core_indexOf = core_deletedIds.indexOf,\n  core_toString = class2type.toString,\n  core_hasOwn = class2type.hasOwnProperty,\n  core_trim = core_version.trim,\n\n  // Define a local copy of jQuery\n  jQuery = function( selector, context ) {\n    // The jQuery object is actually just the init constructor 'enhanced'\n    return new jQuery.fn.init( selector, context, rootjQuery );\n  },\n\n  // Used for matching numbers\n  core_pnum = /[+-]?(?:\\d*\\.|)\\d+(?:[eE][+-]?\\d+|)/.source,\n\n  // Used for splitting on whitespace\n  core_rnotwhite = /\\S+/g,\n\n  // A simple way to check for HTML strings\n  // Prioritize #id over <tag> to avoid XSS via location.hash (#9521)\n  // Strict HTML recognition (#11290: must start with <)\n  rquickExpr = /^(?:\\s*(<[\\w\\W]+>)[^>]*|#([\\w-]*))$/,\n\n  // Match a standalone tag\n  rsingleTag = /^<(\\w+)\\s*\\/?>(?:<\\/\\1>|)$/,\n\n  // Matches dashed string for camelizing\n  rmsPrefix = /^-ms-/,\n  rdashAlpha = /-([\\da-z])/gi,\n\n  // Used by jQuery.camelCase as callback to replace()\n  fcamelCase = function( all, letter ) {\n    return letter.toUpperCase();\n  },\n\n  // The ready event handler and self cleanup method\n  completed = function() {\n    document.removeEventListener( \"DOMContentLoaded\", completed, false );\n    window.removeEventListener( \"load\", completed, false );\n    jQuery.ready();\n  };\n\njQuery.fn = jQuery.prototype = {\n  // The current version of jQuery being used\n  jquery: core_version,\n\n  constructor: jQuery,\n  init: function( selector, context, rootjQuery ) {\n    var match, elem;\n\n    // HANDLE: $(\"\"), $(null), $(undefined), $(false)\n    if ( !selector ) {\n      return this;\n    }\n\n    // Handle HTML strings\n    if ( typeof selector === \"string\" ) {\n      if ( selector.charAt(0) === \"<\" && selector.charAt( selector.length - 1 ) === \">\" && selector.length >= 3 ) {\n        // Assume that strings that start and end with <> are HTML and skip the regex check\n        match = [ null, selector, null ];\n\n      } else {\n        match = rquickExpr.exec( selector );\n      }\n\n      // Match html or make sure no context is specified for #id\n      if ( match && (match[1] || !context) ) {\n\n        // HANDLE: $(html) -> $(array)\n        if ( match[1] ) {\n          context = context instanceof jQuery ? context[0] : context;\n\n          // scripts is true for back-compat\n          jQuery.merge( this, jQuery.parseHTML(\n            match[1],\n            context && context.nodeType ? context.ownerDocument || context : document,\n            true\n          ) );\n\n          // HANDLE: $(html, props)\n          if ( rsingleTag.test( match[1] ) && jQuery.isPlainObject( context ) ) {\n            for ( match in context ) {\n              // Properties of context are called as methods if possible\n              if ( jQuery.isFunction( this[ match ] ) ) {\n                this[ match ]( context[ match ] );\n\n              // ...and otherwise set as attributes\n              } else {\n                this.attr( match, context[ match ] );\n              }\n            }\n          }\n\n          return this;\n\n        // HANDLE: $(#id)\n        } else {\n          elem = document.getElementById( match[2] );\n\n          // Check parentNode to catch when Blackberry 4.6 returns\n          // nodes that are no longer in the document #6963\n          if ( elem && elem.parentNode ) {\n            // Inject the element directly into the jQuery object\n            this.length = 1;\n            this[0] = elem;\n          }\n\n          this.context = document;\n          this.selector = selector;\n          return this;\n        }\n\n      // HANDLE: $(expr, $(...))\n      } else if ( !context || context.jquery ) {\n        return ( context || rootjQuery ).find( selector );\n\n      // HANDLE: $(expr, context)\n      // (which is just equivalent to: $(context).find(expr)\n      } else {\n        return this.constructor( context ).find( selector );\n      }\n\n    // HANDLE: $(DOMElement)\n    } else if ( selector.nodeType ) {\n      this.context = this[0] = selector;\n      this.length = 1;\n      return this;\n\n    // HANDLE: $(function)\n    // Shortcut for document ready\n    } else if ( jQuery.isFunction( selector ) ) {\n      return rootjQuery.ready( selector );\n    }\n\n    if ( selector.selector !== undefined ) {\n      this.selector = selector.selector;\n      this.context = selector.context;\n    }\n\n    return jQuery.makeArray( selector, this );\n  },\n\n  // Start with an empty selector\n  selector: \"\",\n\n  // The default length of a jQuery object is 0\n  length: 0,\n\n  toArray: function() {\n    return core_slice.call( this );\n  },\n\n  // Get the Nth element in the matched element set OR\n  // Get the whole matched element set as a clean array\n  get: function( num ) {\n    return num == null ?\n\n      // Return a 'clean' array\n      this.toArray() :\n\n      // Return just the object\n      ( num < 0 ? this[ this.length + num ] : this[ num ] );\n  },\n\n  // Take an array of elements and push it onto the stack\n  // (returning the new matched element set)\n  pushStack: function( elems ) {\n\n    // Build a new jQuery matched element set\n    var ret = jQuery.merge( this.constructor(), elems );\n\n    // Add the old object onto the stack (as a reference)\n    ret.prevObject = this;\n    ret.context = this.context;\n\n    // Return the newly-formed element set\n    return ret;\n  },\n\n  // Execute a callback for every element in the matched set.\n  // (You can seed the arguments with an array of args, but this is\n  // only used internally.)\n  each: function( callback, args ) {\n    return jQuery.each( this, callback, args );\n  },\n\n  ready: function( fn ) {\n    // Add the callback\n    jQuery.ready.promise().done( fn );\n\n    return this;\n  },\n\n  slice: function() {\n    return this.pushStack( core_slice.apply( this, arguments ) );\n  },\n\n  first: function() {\n    return this.eq( 0 );\n  },\n\n  last: function() {\n    return this.eq( -1 );\n  },\n\n  eq: function( i ) {\n    var len = this.length,\n      j = +i + ( i < 0 ? len : 0 );\n    return this.pushStack( j >= 0 && j < len ? [ this[j] ] : [] );\n  },\n\n  map: function( callback ) {\n    return this.pushStack( jQuery.map(this, function( elem, i ) {\n      return callback.call( elem, i, elem );\n    }));\n  },\n\n  end: function() {\n    return this.prevObject || this.constructor(null);\n  },\n\n  // For internal use only.\n  // Behaves like an Array's method, not like a jQuery method.\n  push: core_push,\n  sort: [].sort,\n  splice: [].splice\n};\n\n// Give the init function the jQuery prototype for later instantiation\njQuery.fn.init.prototype = jQuery.fn;\n\njQuery.extend = jQuery.fn.extend = function() {\n  var options, name, src, copy, copyIsArray, clone,\n    target = arguments[0] || {},\n    i = 1,\n    length = arguments.length,\n    deep = false;\n\n  // Handle a deep copy situation\n  if ( typeof target === \"boolean\" ) {\n    deep = target;\n    target = arguments[1] || {};\n    // skip the boolean and the target\n    i = 2;\n  }\n\n  // Handle case when target is a string or something (possible in deep copy)\n  if ( typeof target !== \"object\" && !jQuery.isFunction(target) ) {\n    target = {};\n  }\n\n  // extend jQuery itself if only one argument is passed\n  if ( length === i ) {\n    target = this;\n    --i;\n  }\n\n  for ( ; i < length; i++ ) {\n    // Only deal with non-null/undefined values\n    if ( (options = arguments[ i ]) != null ) {\n      // Extend the base object\n      for ( name in options ) {\n        src = target[ name ];\n        copy = options[ name ];\n\n        // Prevent never-ending loop\n        if ( target === copy ) {\n          continue;\n        }\n\n        // Recurse if we're merging plain objects or arrays\n        if ( deep && copy && ( jQuery.isPlainObject(copy) || (copyIsArray = jQuery.isArray(copy)) ) ) {\n          if ( copyIsArray ) {\n            copyIsArray = false;\n            clone = src && jQuery.isArray(src) ? src : [];\n\n          } else {\n            clone = src && jQuery.isPlainObject(src) ? src : {};\n          }\n\n          // Never move original objects, clone them\n          target[ name ] = jQuery.extend( deep, clone, copy );\n\n        // Don't bring in undefined values\n        } else if ( copy !== undefined ) {\n          target[ name ] = copy;\n        }\n      }\n    }\n  }\n\n  // Return the modified object\n  return target;\n};\n\njQuery.extend({\n  // Unique for each copy of jQuery on the page\n  expando: \"jQuery\" + ( core_version + Math.random() ).replace( /\\D/g, \"\" ),\n\n  noConflict: function( deep ) {\n    if ( window.$ === jQuery ) {\n      window.$ = _$;\n    }\n\n    if ( deep && window.jQuery === jQuery ) {\n      window.jQuery = _jQuery;\n    }\n\n    return jQuery;\n  },\n\n  // Is the DOM ready to be used? Set to true once it occurs.\n  isReady: false,\n\n  // A counter to track how many items to wait for before\n  // the ready event fires. See #6781\n  readyWait: 1,\n\n  // Hold (or release) the ready event\n  holdReady: function( hold ) {\n    if ( hold ) {\n      jQuery.readyWait++;\n    } else {\n      jQuery.ready( true );\n    }\n  },\n\n  // Handle when the DOM is ready\n  ready: function( wait ) {\n\n    // Abort if there are pending holds or we're already ready\n    if ( wait === true ? --jQuery.readyWait : jQuery.isReady ) {\n      return;\n    }\n\n    // Remember that the DOM is ready\n    jQuery.isReady = true;\n\n    // If a normal DOM Ready event fired, decrement, and wait if need be\n    if ( wait !== true && --jQuery.readyWait > 0 ) {\n      return;\n    }\n\n    // If there are functions bound, to execute\n    readyList.resolveWith( document, [ jQuery ] );\n\n    // Trigger any bound ready events\n    if ( jQuery.fn.trigger ) {\n      jQuery( document ).trigger(\"ready\").off(\"ready\");\n    }\n  },\n\n  // See test/unit/core.js for details concerning isFunction.\n  // Since version 1.3, DOM methods and functions like alert\n  // aren't supported. They return false on IE (#2968).\n  isFunction: function( obj ) {\n    return jQuery.type(obj) === \"function\";\n  },\n\n  isArray: Array.isArray,\n\n  isWindow: function( obj ) {\n    return obj != null && obj === obj.window;\n  },\n\n  isNumeric: function( obj ) {\n    return !isNaN( parseFloat(obj) ) && isFinite( obj );\n  },\n\n  type: function( obj ) {\n    if ( obj == null ) {\n      return String( obj );\n    }\n    // Support: Safari <= 5.1 (functionish RegExp)\n    return typeof obj === \"object\" || typeof obj === \"function\" ?\n      class2type[ core_toString.call(obj) ] || \"object\" :\n      typeof obj;\n  },\n\n  isPlainObject: function( obj ) {\n    // Not plain objects:\n    // - Any object or value whose internal [[Class]] property is not \"[object Object]\"\n    // - DOM nodes\n    // - window\n    if ( jQuery.type( obj ) !== \"object\" || obj.nodeType || jQuery.isWindow( obj ) ) {\n      return false;\n    }\n\n    // Support: Firefox <20\n    // The try/catch suppresses exceptions thrown when attempting to access\n    // the \"constructor\" property of certain host objects, ie. |window.location|\n    // https://bugzilla.mozilla.org/show_bug.cgi?id=814622\n    try {\n      if ( obj.constructor &&\n          !core_hasOwn.call( obj.constructor.prototype, \"isPrototypeOf\" ) ) {\n        return false;\n      }\n    } catch ( e ) {\n      return false;\n    }\n\n    // If the function hasn't returned already, we're confident that\n    // |obj| is a plain object, created by {} or constructed with new Object\n    return true;\n  },\n\n  isEmptyObject: function( obj ) {\n    var name;\n    for ( name in obj ) {\n      return false;\n    }\n    return true;\n  },\n\n  error: function( msg ) {\n    throw new Error( msg );\n  },\n\n  // data: string of html\n  // context (optional): If specified, the fragment will be created in this context, defaults to document\n  // keepScripts (optional): If true, will include scripts passed in the html string\n  parseHTML: function( data, context, keepScripts ) {\n    if ( !data || typeof data !== \"string\" ) {\n      return null;\n    }\n    if ( typeof context === \"boolean\" ) {\n      keepScripts = context;\n      context = false;\n    }\n    context = context || document;\n\n    var parsed = rsingleTag.exec( data ),\n      scripts = !keepScripts && [];\n\n    // Single tag\n    if ( parsed ) {\n      return [ context.createElement( parsed[1] ) ];\n    }\n\n    parsed = jQuery.buildFragment( [ data ], context, scripts );\n\n    if ( scripts ) {\n      jQuery( scripts ).remove();\n    }\n\n    return jQuery.merge( [], parsed.childNodes );\n  },\n\n  parseJSON: JSON.parse,\n\n  // Cross-browser xml parsing\n  parseXML: function( data ) {\n    var xml, tmp;\n    if ( !data || typeof data !== \"string\" ) {\n      return null;\n    }\n\n    // Support: IE9\n    try {\n      tmp = new DOMParser();\n      xml = tmp.parseFromString( data , \"text/xml\" );\n    } catch ( e ) {\n      xml = undefined;\n    }\n\n    if ( !xml || xml.getElementsByTagName( \"parsererror\" ).length ) {\n      jQuery.error( \"Invalid XML: \" + data );\n    }\n    return xml;\n  },\n\n  noop: function() {},\n\n  // Evaluates a script in a global context\n  globalEval: function( code ) {\n    var script,\n        indirect = eval;\n\n    code = jQuery.trim( code );\n\n    if ( code ) {\n      // If the code includes a valid, prologue position\n      // strict mode pragma, execute code by injecting a\n      // script tag into the document.\n      if ( code.indexOf(\"use strict\") === 1 ) {\n        script = document.createElement(\"script\");\n        script.text = code;\n        document.head.appendChild( script ).parentNode.removeChild( script );\n      } else {\n      // Otherwise, avoid the DOM node creation, insertion\n      // and removal by using an indirect global eval\n        indirect( code );\n      }\n    }\n  },\n\n  // Convert dashed to camelCase; used by the css and data modules\n  // Microsoft forgot to hump their vendor prefix (#9572)\n  camelCase: function( string ) {\n    return string.replace( rmsPrefix, \"ms-\" ).replace( rdashAlpha, fcamelCase );\n  },\n\n  nodeName: function( elem, name ) {\n    return elem.nodeName && elem.nodeName.toLowerCase() === name.toLowerCase();\n  },\n\n  // args is for internal usage only\n  each: function( obj, callback, args ) {\n    var value,\n      i = 0,\n      length = obj.length,\n      isArray = isArraylike( obj );\n\n    if ( args ) {\n      if ( isArray ) {\n        for ( ; i < length; i++ ) {\n          value = callback.apply( obj[ i ], args );\n\n          if ( value === false ) {\n            break;\n          }\n        }\n      } else {\n        for ( i in obj ) {\n          value = callback.apply( obj[ i ], args );\n\n          if ( value === false ) {\n            break;\n          }\n        }\n      }\n\n    // A special, fast, case for the most common use of each\n    } else {\n      if ( isArray ) {\n        for ( ; i < length; i++ ) {\n          value = callback.call( obj[ i ], i, obj[ i ] );\n\n          if ( value === false ) {\n            break;\n          }\n        }\n      } else {\n        for ( i in obj ) {\n          value = callback.call( obj[ i ], i, obj[ i ] );\n\n          if ( value === false ) {\n            break;\n          }\n        }\n      }\n    }\n\n    return obj;\n  },\n\n  trim: function( text ) {\n    return text == null ? \"\" : core_trim.call( text );\n  },\n\n  // results is for internal usage only\n  makeArray: function( arr, results ) {\n    var ret = results || [];\n\n    if ( arr != null ) {\n      if ( isArraylike( Object(arr) ) ) {\n        jQuery.merge( ret,\n          typeof arr === \"string\" ?\n          [ arr ] : arr\n        );\n      } else {\n        core_push.call( ret, arr );\n      }\n    }\n\n    return ret;\n  },\n\n  inArray: function( elem, arr, i ) {\n    return arr == null ? -1 : core_indexOf.call( arr, elem, i );\n  },\n\n  merge: function( first, second ) {\n    var l = second.length,\n      i = first.length,\n      j = 0;\n\n    if ( typeof l === \"number\" ) {\n      for ( ; j < l; j++ ) {\n        first[ i++ ] = second[ j ];\n      }\n    } else {\n      while ( second[j] !== undefined ) {\n        first[ i++ ] = second[ j++ ];\n      }\n    }\n\n    first.length = i;\n\n    return first;\n  },\n\n  grep: function( elems, callback, inv ) {\n    var retVal,\n      ret = [],\n      i = 0,\n      length = elems.length;\n    inv = !!inv;\n\n    // Go through the array, only saving the items\n    // that pass the validator function\n    for ( ; i < length; i++ ) {\n      retVal = !!callback( elems[ i ], i );\n      if ( inv !== retVal ) {\n        ret.push( elems[ i ] );\n      }\n    }\n\n    return ret;\n  },\n\n  // arg is for internal usage only\n  map: function( elems, callback, arg ) {\n    var value,\n      i = 0,\n      length = elems.length,\n      isArray = isArraylike( elems ),\n      ret = [];\n\n    // Go through the array, translating each of the items to their\n    if ( isArray ) {\n      for ( ; i < length; i++ ) {\n        value = callback( elems[ i ], i, arg );\n\n        if ( value != null ) {\n          ret[ ret.length ] = value;\n        }\n      }\n\n    // Go through every key on the object,\n    } else {\n      for ( i in elems ) {\n        value = callback( elems[ i ], i, arg );\n\n        if ( value != null ) {\n          ret[ ret.length ] = value;\n        }\n      }\n    }\n\n    // Flatten any nested arrays\n    return core_concat.apply( [], ret );\n  },\n\n  // A global GUID counter for objects\n  guid: 1,\n\n  // Bind a function to a context, optionally partially applying any\n  // arguments.\n  proxy: function( fn, context ) {\n    var tmp, args, proxy;\n\n    if ( typeof context === \"string\" ) {\n      tmp = fn[ context ];\n      context = fn;\n      fn = tmp;\n    }\n\n    // Quick check to determine if target is callable, in the spec\n    // this throws a TypeError, but we will just return undefined.\n    if ( !jQuery.isFunction( fn ) ) {\n      return undefined;\n    }\n\n    // Simulated bind\n    args = core_slice.call( arguments, 2 );\n    proxy = function() {\n      return fn.apply( context || this, args.concat( core_slice.call( arguments ) ) );\n    };\n\n    // Set the guid of unique handler to the same of original handler, so it can be removed\n    proxy.guid = fn.guid = fn.guid || jQuery.guid++;\n\n    return proxy;\n  },\n\n  // Multifunctional method to get and set values of a collection\n  // The value/s can optionally be executed if it's a function\n  access: function( elems, fn, key, value, chainable, emptyGet, raw ) {\n    var i = 0,\n      length = elems.length,\n      bulk = key == null;\n\n    // Sets many values\n    if ( jQuery.type( key ) === \"object\" ) {\n      chainable = true;\n      for ( i in key ) {\n        jQuery.access( elems, fn, i, key[i], true, emptyGet, raw );\n      }\n\n    // Sets one value\n    } else if ( value !== undefined ) {\n      chainable = true;\n\n      if ( !jQuery.isFunction( value ) ) {\n        raw = true;\n      }\n\n      if ( bulk ) {\n        // Bulk operations run against the entire set\n        if ( raw ) {\n          fn.call( elems, value );\n          fn = null;\n\n        // ...except when executing function values\n        } else {\n          bulk = fn;\n          fn = function( elem, key, value ) {\n            return bulk.call( jQuery( elem ), value );\n          };\n        }\n      }\n\n      if ( fn ) {\n        for ( ; i < length; i++ ) {\n          fn( elems[i], key, raw ? value : value.call( elems[i], i, fn( elems[i], key ) ) );\n        }\n      }\n    }\n\n    return chainable ?\n      elems :\n\n      // Gets\n      bulk ?\n        fn.call( elems ) :\n        length ? fn( elems[0], key ) : emptyGet;\n  },\n\n  now: Date.now,\n\n  // A method for quickly swapping in/out CSS properties to get correct calculations.\n  // Note: this method belongs to the css module but it's needed here for the support module.\n  // If support gets modularized, this method should be moved back to the css module.\n  swap: function( elem, options, callback, args ) {\n    var ret, name,\n      old = {};\n\n    // Remember the old values, and insert the new ones\n    for ( name in options ) {\n      old[ name ] = elem.style[ name ];\n      elem.style[ name ] = options[ name ];\n    }\n\n    ret = callback.apply( elem, args || [] );\n\n    // Revert the old values\n    for ( name in options ) {\n      elem.style[ name ] = old[ name ];\n    }\n\n    return ret;\n  }\n});\n\njQuery.ready.promise = function( obj ) {\n  if ( !readyList ) {\n\n    readyList = jQuery.Deferred();\n\n    // Catch cases where $(document).ready() is called after the browser event has already occurred.\n    // we once tried to use readyState \"interactive\" here, but it caused issues like the one\n    // discovered by ChrisS here: http://bugs.jquery.com/ticket/12282#comment:15\n    if ( document.readyState === \"complete\" ) {\n      // Handle it asynchronously to allow scripts the opportunity to delay ready\n      setTimeout( jQuery.ready );\n\n    } else {\n\n      // Use the handy event callback\n      document.addEventListener( \"DOMContentLoaded\", completed, false );\n\n      // A fallback to window.onload, that will always work\n      window.addEventListener( \"load\", completed, false );\n    }\n  }\n  return readyList.promise( obj );\n};\n\n// Populate the class2type map\njQuery.each(\"Boolean Number String Function Array Date RegExp Object Error\".split(\" \"), function(i, name) {\n  class2type[ \"[object \" + name + \"]\" ] = name.toLowerCase();\n});\n\nfunction isArraylike( obj ) {\n  var length = obj.length,\n    type = jQuery.type( obj );\n\n  if ( jQuery.isWindow( obj ) ) {\n    return false;\n  }\n\n  if ( obj.nodeType === 1 && length ) {\n    return true;\n  }\n\n  return type === \"array\" || type !== \"function\" &&\n    ( length === 0 ||\n    typeof length === \"number\" && length > 0 && ( length - 1 ) in obj );\n}\n\n// All jQuery objects should point back to these\nrootjQuery = jQuery(document);\n/*!\n * Sizzle CSS Selector Engine v1.9.4-pre\n * http://sizzlejs.com/\n *\n * Copyright 2013 jQuery Foundation, Inc. and other contributors\n * Released under the MIT license\n * http://jquery.org/license\n *\n * Date: 2013-06-03\n */\n(function( window, undefined ) {\n\nvar i,\n  support,\n  cachedruns,\n  Expr,\n  getText,\n  isXML,\n  compile,\n  outermostContext,\n  sortInput,\n\n  // Local document vars\n  setDocument,\n  document,\n  docElem,\n  documentIsHTML,\n  rbuggyQSA,\n  rbuggyMatches,\n  matches,\n  contains,\n\n  // Instance-specific data\n  expando = \"sizzle\" + -(new Date()),\n  preferredDoc = window.document,\n  dirruns = 0,\n  done = 0,\n  classCache = createCache(),\n  tokenCache = createCache(),\n  compilerCache = createCache(),\n  hasDuplicate = false,\n  sortOrder = function( a, b ) {\n    if ( a === b ) {\n      hasDuplicate = true;\n      return 0;\n    }\n    return 0;\n  },\n\n  // General-purpose constants\n  strundefined = typeof undefined,\n  MAX_NEGATIVE = 1 << 31,\n\n  // Instance methods\n  hasOwn = ({}).hasOwnProperty,\n  arr = [],\n  pop = arr.pop,\n  push_native = arr.push,\n  push = arr.push,\n  slice = arr.slice,\n  // Use a stripped-down indexOf if we can't use a native one\n  indexOf = arr.indexOf || function( elem ) {\n    var i = 0,\n      len = this.length;\n    for ( ; i < len; i++ ) {\n      if ( this[i] === elem ) {\n        return i;\n      }\n    }\n    return -1;\n  },\n\n  booleans = \"checked|selected|async|autofocus|autoplay|controls|defer|disabled|hidden|ismap|loop|multiple|open|readonly|required|scoped\",\n\n  // Regular expressions\n\n  // Whitespace characters http://www.w3.org/TR/css3-selectors/#whitespace\n  whitespace = \"[\\\\x20\\\\t\\\\r\\\\n\\\\f]\",\n  // http://www.w3.org/TR/css3-syntax/#characters\n  characterEncoding = \"(?:\\\\\\\\.|[\\\\w-]|[^\\\\x00-\\\\xa0])+\",\n\n  // Loosely modeled on CSS identifier characters\n  // An unquoted value should be a CSS identifier http://www.w3.org/TR/css3-selectors/#attribute-selectors\n  // Proper syntax: http://www.w3.org/TR/CSS21/syndata.html#value-def-identifier\n  identifier = characterEncoding.replace( \"w\", \"w#\" ),\n\n  // Acceptable operators http://www.w3.org/TR/selectors/#attribute-selectors\n  attributes = \"\\\\[\" + whitespace + \"*(\" + characterEncoding + \")\" + whitespace +\n    \"*(?:([*^$|!~]?=)\" + whitespace + \"*(?:(['\\\"])((?:\\\\\\\\.|[^\\\\\\\\])*?)\\\\3|(\" + identifier + \")|)|)\" + whitespace + \"*\\\\]\",\n\n  // Prefer arguments quoted,\n  //   then not containing pseudos/brackets,\n  //   then attribute selectors/non-parenthetical expressions,\n  //   then anything else\n  // These preferences are here to reduce the number of selectors\n  //   needing tokenize in the PSEUDO preFilter\n  pseudos = \":(\" + characterEncoding + \")(?:\\\\(((['\\\"])((?:\\\\\\\\.|[^\\\\\\\\])*?)\\\\3|((?:\\\\\\\\.|[^\\\\\\\\()[\\\\]]|\" + attributes.replace( 3, 8 ) + \")*)|.*)\\\\)|)\",\n\n  // Leading and non-escaped trailing whitespace, capturing some non-whitespace characters preceding the latter\n  rtrim = new RegExp( \"^\" + whitespace + \"+|((?:^|[^\\\\\\\\])(?:\\\\\\\\.)*)\" + whitespace + \"+$\", \"g\" ),\n\n  rcomma = new RegExp( \"^\" + whitespace + \"*,\" + whitespace + \"*\" ),\n  rcombinators = new RegExp( \"^\" + whitespace + \"*([>+~]|\" + whitespace + \")\" + whitespace + \"*\" ),\n\n  rsibling = new RegExp( whitespace + \"*[+~]\" ),\n  rattributeQuotes = new RegExp( \"=\" + whitespace + \"*([^\\\\]'\\\"]*)\" + whitespace + \"*\\\\]\", \"g\" ),\n\n  rpseudo = new RegExp( pseudos ),\n  ridentifier = new RegExp( \"^\" + identifier + \"$\" ),\n\n  matchExpr = {\n    \"ID\": new RegExp( \"^#(\" + characterEncoding + \")\" ),\n    \"CLASS\": new RegExp( \"^\\\\.(\" + characterEncoding + \")\" ),\n    \"TAG\": new RegExp( \"^(\" + characterEncoding.replace( \"w\", \"w*\" ) + \")\" ),\n    \"ATTR\": new RegExp( \"^\" + attributes ),\n    \"PSEUDO\": new RegExp( \"^\" + pseudos ),\n    \"CHILD\": new RegExp( \"^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\\\(\" + whitespace +\n      \"*(even|odd|(([+-]|)(\\\\d*)n|)\" + whitespace + \"*(?:([+-]|)\" + whitespace +\n      \"*(\\\\d+)|))\" + whitespace + \"*\\\\)|)\", \"i\" ),\n    \"bool\": new RegExp( \"^(?:\" + booleans + \")$\", \"i\" ),\n    // For use in libraries implementing .is()\n    // We use this for POS matching in `select`\n    \"needsContext\": new RegExp( \"^\" + whitespace + \"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\\\(\" +\n      whitespace + \"*((?:-\\\\d)?\\\\d*)\" + whitespace + \"*\\\\)|)(?=[^-]|$)\", \"i\" )\n  },\n\n  rnative = /^[^{]+\\{\\s*\\[native \\w/,\n\n  // Easily-parseable/retrievable ID or TAG or CLASS selectors\n  rquickExpr = /^(?:#([\\w-]+)|(\\w+)|\\.([\\w-]+))$/,\n\n  rinputs = /^(?:input|select|textarea|button)$/i,\n  rheader = /^h\\d$/i,\n\n  rescape = /'|\\\\/g,\n\n  // CSS escapes http://www.w3.org/TR/CSS21/syndata.html#escaped-characters\n  runescape = new RegExp( \"\\\\\\\\([\\\\da-f]{1,6}\" + whitespace + \"?|(\" + whitespace + \")|.)\", \"ig\" ),\n  funescape = function( _, escaped, escapedWhitespace ) {\n    var high = \"0x\" + escaped - 0x10000;\n    // NaN means non-codepoint\n    // Support: Firefox\n    // Workaround erroneous numeric interpretation of +\"0x\"\n    return high !== high || escapedWhitespace ?\n      escaped :\n      // BMP codepoint\n      high < 0 ?\n        String.fromCharCode( high + 0x10000 ) :\n        // Supplemental Plane codepoint (surrogate pair)\n        String.fromCharCode( high >> 10 | 0xD800, high & 0x3FF | 0xDC00 );\n  };\n\n// Optimize for push.apply( _, NodeList )\ntry {\n  push.apply(\n    (arr = slice.call( preferredDoc.childNodes )),\n    preferredDoc.childNodes\n  );\n  // Support: Android<4.0\n  // Detect silently failing push.apply\n  arr[ preferredDoc.childNodes.length ].nodeType;\n} catch ( e ) {\n  push = { apply: arr.length ?\n\n    // Leverage slice if possible\n    function( target, els ) {\n      push_native.apply( target, slice.call(els) );\n    } :\n\n    // Support: IE<9\n    // Otherwise append directly\n    function( target, els ) {\n      var j = target.length,\n        i = 0;\n      // Can't trust NodeList.length\n      while ( (target[j++] = els[i++]) ) {}\n      target.length = j - 1;\n    }\n  };\n}\n\nfunction Sizzle( selector, context, results, seed ) {\n  var match, elem, m, nodeType,\n    // QSA vars\n    i, groups, old, nid, newContext, newSelector;\n\n  if ( ( context ? context.ownerDocument || context : preferredDoc ) !== document ) {\n    setDocument( context );\n  }\n\n  context = context || document;\n  results = results || [];\n\n  if ( !selector || typeof selector !== \"string\" ) {\n    return results;\n  }\n\n  if ( (nodeType = context.nodeType) !== 1 && nodeType !== 9 ) {\n    return [];\n  }\n\n  if ( documentIsHTML && !seed ) {\n\n    // Shortcuts\n    if ( (match = rquickExpr.exec( selector )) ) {\n      // Speed-up: Sizzle(\"#ID\")\n      if ( (m = match[1]) ) {\n        if ( nodeType === 9 ) {\n          elem = context.getElementById( m );\n          // Check parentNode to catch when Blackberry 4.6 returns\n          // nodes that are no longer in the document #6963\n          if ( elem && elem.parentNode ) {\n            // Handle the case where IE, Opera, and Webkit return items\n            // by name instead of ID\n            if ( elem.id === m ) {\n              results.push( elem );\n              return results;\n            }\n          } else {\n            return results;\n          }\n        } else {\n          // Context is not a document\n          if ( context.ownerDocument && (elem = context.ownerDocument.getElementById( m )) &&\n            contains( context, elem ) && elem.id === m ) {\n            results.push( elem );\n            return results;\n          }\n        }\n\n      // Speed-up: Sizzle(\"TAG\")\n      } else if ( match[2] ) {\n        push.apply( results, context.getElementsByTagName( selector ) );\n        return results;\n\n      // Speed-up: Sizzle(\".CLASS\")\n      } else if ( (m = match[3]) && support.getElementsByClassName && context.getElementsByClassName ) {\n        push.apply( results, context.getElementsByClassName( m ) );\n        return results;\n      }\n    }\n\n    // QSA path\n    if ( support.qsa && (!rbuggyQSA || !rbuggyQSA.test( selector )) ) {\n      nid = old = expando;\n      newContext = context;\n      newSelector = nodeType === 9 && selector;\n\n      // qSA works strangely on Element-rooted queries\n      // We can work around this by specifying an extra ID on the root\n      // and working up from there (Thanks to Andrew Dupont for the technique)\n      // IE 8 doesn't work on object elements\n      if ( nodeType === 1 && context.nodeName.toLowerCase() !== \"object\" ) {\n        groups = tokenize( selector );\n\n        if ( (old = context.getAttribute(\"id\")) ) {\n          nid = old.replace( rescape, \"\\\\$&\" );\n        } else {\n          context.setAttribute( \"id\", nid );\n        }\n        nid = \"[id='\" + nid + \"'] \";\n\n        i = groups.length;\n        while ( i-- ) {\n          groups[i] = nid + toSelector( groups[i] );\n        }\n        newContext = rsibling.test( selector ) && context.parentNode || context;\n        newSelector = groups.join(\",\");\n      }\n\n      if ( newSelector ) {\n        try {\n          push.apply( results,\n            newContext.querySelectorAll( newSelector )\n          );\n          return results;\n        } catch(qsaError) {\n        } finally {\n          if ( !old ) {\n            context.removeAttribute(\"id\");\n          }\n        }\n      }\n    }\n  }\n\n  // All others\n  return select( selector.replace( rtrim, \"$1\" ), context, results, seed );\n}\n\n/**\n * Create key-value caches of limited size\n * @returns {Function(string, Object)} Returns the Object data after storing it on itself with\n *  property name the (space-suffixed) string and (if the cache is larger than Expr.cacheLength)\n *  deleting the oldest entry\n */\nfunction createCache() {\n  var keys = [];\n\n  function cache( key, value ) {\n    // Use (key + \" \") to avoid collision with native prototype properties (see Issue #157)\n    if ( keys.push( key += \" \" ) > Expr.cacheLength ) {\n      // Only keep the most recent entries\n      delete cache[ keys.shift() ];\n    }\n    return (cache[ key ] = value);\n  }\n  return cache;\n}\n\n/**\n * Mark a function for special use by Sizzle\n * @param {Function} fn The function to mark\n */\nfunction markFunction( fn ) {\n  fn[ expando ] = true;\n  return fn;\n}\n\n/**\n * Support testing using an element\n * @param {Function} fn Passed the created div and expects a boolean result\n */\nfunction assert( fn ) {\n  var div = document.createElement(\"div\");\n\n  try {\n    return !!fn( div );\n  } catch (e) {\n    return false;\n  } finally {\n    // Remove from its parent by default\n    if ( div.parentNode ) {\n      div.parentNode.removeChild( div );\n    }\n    // release memory in IE\n    div = null;\n  }\n}\n\n/**\n * Adds the same handler for all of the specified attrs\n * @param {String} attrs Pipe-separated list of attributes\n * @param {Function} handler The method that will be applied\n */\nfunction addHandle( attrs, handler ) {\n  var arr = attrs.split(\"|\"),\n    i = attrs.length;\n\n  while ( i-- ) {\n    Expr.attrHandle[ arr[i] ] = handler;\n  }\n}\n\n/**\n * Checks document order of two siblings\n * @param {Element} a\n * @param {Element} b\n * @returns {Number} Returns less than 0 if a precedes b, greater than 0 if a follows b\n */\nfunction siblingCheck( a, b ) {\n  var cur = b && a,\n    diff = cur && a.nodeType === 1 && b.nodeType === 1 &&\n      ( ~b.sourceIndex || MAX_NEGATIVE ) -\n      ( ~a.sourceIndex || MAX_NEGATIVE );\n\n  // Use IE sourceIndex if available on both nodes\n  if ( diff ) {\n    return diff;\n  }\n\n  // Check if b follows a\n  if ( cur ) {\n    while ( (cur = cur.nextSibling) ) {\n      if ( cur === b ) {\n        return -1;\n      }\n    }\n  }\n\n  return a ? 1 : -1;\n}\n\n/**\n * Returns a function to use in pseudos for input types\n * @param {String} type\n */\nfunction createInputPseudo( type ) {\n  return function( elem ) {\n    var name = elem.nodeName.toLowerCase();\n    return name === \"input\" && elem.type === type;\n  };\n}\n\n/**\n * Returns a function to use in pseudos for buttons\n * @param {String} type\n */\nfunction createButtonPseudo( type ) {\n  return function( elem ) {\n    var name = elem.nodeName.toLowerCase();\n    return (name === \"input\" || name === \"button\") && elem.type === type;\n  };\n}\n\n/**\n * Returns a function to use in pseudos for positionals\n * @param {Function} fn\n */\nfunction createPositionalPseudo( fn ) {\n  return markFunction(function( argument ) {\n    argument = +argument;\n    return markFunction(function( seed, matches ) {\n      var j,\n        matchIndexes = fn( [], seed.length, argument ),\n        i = matchIndexes.length;\n\n      // Match elements found at the specified indexes\n      while ( i-- ) {\n        if ( seed[ (j = matchIndexes[i]) ] ) {\n          seed[j] = !(matches[j] = seed[j]);\n        }\n      }\n    });\n  });\n}\n\n/**\n * Detect xml\n * @param {Element|Object} elem An element or a document\n */\nisXML = Sizzle.isXML = function( elem ) {\n  // documentElement is verified for cases where it doesn't yet exist\n  // (such as loading iframes in IE - #4833)\n  var documentElement = elem && (elem.ownerDocument || elem).documentElement;\n  return documentElement ? documentElement.nodeName !== \"HTML\" : false;\n};\n\n// Expose support vars for convenience\nsupport = Sizzle.support = {};\n\n/**\n * Sets document-related variables once based on the current document\n * @param {Element|Object} [doc] An element or document object to use to set the document\n * @returns {Object} Returns the current document\n */\nsetDocument = Sizzle.setDocument = function( node ) {\n  var doc = node ? node.ownerDocument || node : preferredDoc,\n    parent = doc.defaultView;\n\n  // If no document and documentElement is available, return\n  if ( doc === document || doc.nodeType !== 9 || !doc.documentElement ) {\n    return document;\n  }\n\n  // Set our document\n  document = doc;\n  docElem = doc.documentElement;\n\n  // Support tests\n  documentIsHTML = !isXML( doc );\n\n  // Support: IE>8\n  // If iframe document is assigned to \"document\" variable and if iframe has been reloaded,\n  // IE will throw \"permission denied\" error when accessing \"document\" variable, see jQuery #13936\n  // IE6-8 do not support the defaultView property so parent will be undefined\n  if ( parent && parent.attachEvent && parent !== parent.top ) {\n    parent.attachEvent( \"onbeforeunload\", function() {\n      setDocument();\n    });\n  }\n\n  /* Attributes\n  ---------------------------------------------------------------------- */\n\n  // Support: IE<8\n  // Verify that getAttribute really returns attributes and not properties (excepting IE8 booleans)\n  support.attributes = assert(function( div ) {\n    div.className = \"i\";\n    return !div.getAttribute(\"className\");\n  });\n\n  /* getElement(s)By*\n  ---------------------------------------------------------------------- */\n\n  // Check if getElementsByTagName(\"*\") returns only elements\n  support.getElementsByTagName = assert(function( div ) {\n    div.appendChild( doc.createComment(\"\") );\n    return !div.getElementsByTagName(\"*\").length;\n  });\n\n  // Check if getElementsByClassName can be trusted\n  support.getElementsByClassName = assert(function( div ) {\n    div.innerHTML = \"<div class='a'></div><div class='a i'></div>\";\n\n    // Support: Safari<4\n    // Catch class over-caching\n    div.firstChild.className = \"i\";\n    // Support: Opera<10\n    // Catch gEBCN failure to find non-leading classes\n    return div.getElementsByClassName(\"i\").length === 2;\n  });\n\n  // Support: IE<10\n  // Check if getElementById returns elements by name\n  // The broken getElementById methods don't pick up programatically-set names,\n  // so use a roundabout getElementsByName test\n  support.getById = assert(function( div ) {\n    docElem.appendChild( div ).id = expando;\n    return !doc.getElementsByName || !doc.getElementsByName( expando ).length;\n  });\n\n  // ID find and filter\n  if ( support.getById ) {\n    Expr.find[\"ID\"] = function( id, context ) {\n      if ( typeof context.getElementById !== strundefined && documentIsHTML ) {\n        var m = context.getElementById( id );\n        // Check parentNode to catch when Blackberry 4.6 returns\n        // nodes that are no longer in the document #6963\n        return m && m.parentNode ? [m] : [];\n      }\n    };\n    Expr.filter[\"ID\"] = function( id ) {\n      var attrId = id.replace( runescape, funescape );\n      return function( elem ) {\n        return elem.getAttribute(\"id\") === attrId;\n      };\n    };\n  } else {\n    // Support: IE6/7\n    // getElementById is not reliable as a find shortcut\n    delete Expr.find[\"ID\"];\n\n    Expr.filter[\"ID\"] =  function( id ) {\n      var attrId = id.replace( runescape, funescape );\n      return function( elem ) {\n        var node = typeof elem.getAttributeNode !== strundefined && elem.getAttributeNode(\"id\");\n        return node && node.value === attrId;\n      };\n    };\n  }\n\n  // Tag\n  Expr.find[\"TAG\"] = support.getElementsByTagName ?\n    function( tag, context ) {\n      if ( typeof context.getElementsByTagName !== strundefined ) {\n        return context.getElementsByTagName( tag );\n      }\n    } :\n    function( tag, context ) {\n      var elem,\n        tmp = [],\n        i = 0,\n        results = context.getElementsByTagName( tag );\n\n      // Filter out possible comments\n      if ( tag === \"*\" ) {\n        while ( (elem = results[i++]) ) {\n          if ( elem.nodeType === 1 ) {\n            tmp.push( elem );\n          }\n        }\n\n        return tmp;\n      }\n      return results;\n    };\n\n  // Class\n  Expr.find[\"CLASS\"] = support.getElementsByClassName && function( className, context ) {\n    if ( typeof context.getElementsByClassName !== strundefined && documentIsHTML ) {\n      return context.getElementsByClassName( className );\n    }\n  };\n\n  /* QSA/matchesSelector\n  ---------------------------------------------------------------------- */\n\n  // QSA and matchesSelector support\n\n  // matchesSelector(:active) reports false when true (IE9/Opera 11.5)\n  rbuggyMatches = [];\n\n  // qSa(:focus) reports false when true (Chrome 21)\n  // We allow this because of a bug in IE8/9 that throws an error\n  // whenever `document.activeElement` is accessed on an iframe\n  // So, we allow :focus to pass through QSA all the time to avoid the IE error\n  // See http://bugs.jquery.com/ticket/13378\n  rbuggyQSA = [];\n\n  if ( (support.qsa = rnative.test( doc.querySelectorAll )) ) {\n    // Build QSA regex\n    // Regex strategy adopted from Diego Perini\n    assert(function( div ) {\n      // Select is set to empty string on purpose\n      // This is to test IE's treatment of not explicitly\n      // setting a boolean content attribute,\n      // since its presence should be enough\n      // http://bugs.jquery.com/ticket/12359\n      div.innerHTML = \"<select><option selected=''></option></select>\";\n\n      // Support: IE8\n      // Boolean attributes and \"value\" are not treated correctly\n      if ( !div.querySelectorAll(\"[selected]\").length ) {\n        rbuggyQSA.push( \"\\\\[\" + whitespace + \"*(?:value|\" + booleans + \")\" );\n      }\n\n      // Webkit/Opera - :checked should return selected option elements\n      // http://www.w3.org/TR/2011/REC-css3-selectors-20110929/#checked\n      // IE8 throws error here and will not see later tests\n      if ( !div.querySelectorAll(\":checked\").length ) {\n        rbuggyQSA.push(\":checked\");\n      }\n    });\n\n    assert(function( div ) {\n\n      // Support: Opera 10-12/IE8\n      // ^= $= *= and empty values\n      // Should not select anything\n      // Support: Windows 8 Native Apps\n      // The type attribute is restricted during .innerHTML assignment\n      var input = doc.createElement(\"input\");\n      input.setAttribute( \"type\", \"hidden\" );\n      div.appendChild( input ).setAttribute( \"t\", \"\" );\n\n      if ( div.querySelectorAll(\"[t^='']\").length ) {\n        rbuggyQSA.push( \"[*^$]=\" + whitespace + \"*(?:''|\\\"\\\")\" );\n      }\n\n      // FF 3.5 - :enabled/:disabled and hidden elements (hidden elements are still enabled)\n      // IE8 throws error here and will not see later tests\n      if ( !div.querySelectorAll(\":enabled\").length ) {\n        rbuggyQSA.push( \":enabled\", \":disabled\" );\n      }\n\n      // Opera 10-11 does not throw on post-comma invalid pseudos\n      div.querySelectorAll(\"*,:x\");\n      rbuggyQSA.push(\",.*:\");\n    });\n  }\n\n  if ( (support.matchesSelector = rnative.test( (matches = docElem.webkitMatchesSelector ||\n    docElem.mozMatchesSelector ||\n    docElem.oMatchesSelector ||\n    docElem.msMatchesSelector) )) ) {\n\n    assert(function( div ) {\n      // Check to see if it's possible to do matchesSelector\n      // on a disconnected node (IE 9)\n      support.disconnectedMatch = matches.call( div, \"div\" );\n\n      // This should fail with an exception\n      // Gecko does not error, returns false instead\n      matches.call( div, \"[s!='']:x\" );\n      rbuggyMatches.push( \"!=\", pseudos );\n    });\n  }\n\n  rbuggyQSA = rbuggyQSA.length && new RegExp( rbuggyQSA.join(\"|\") );\n  rbuggyMatches = rbuggyMatches.length && new RegExp( rbuggyMatches.join(\"|\") );\n\n  /* Contains\n  ---------------------------------------------------------------------- */\n\n  // Element contains another\n  // Purposefully does not implement inclusive descendent\n  // As in, an element does not contain itself\n  contains = rnative.test( docElem.contains ) || docElem.compareDocumentPosition ?\n    function( a, b ) {\n      var adown = a.nodeType === 9 ? a.documentElement : a,\n        bup = b && b.parentNode;\n      return a === bup || !!( bup && bup.nodeType === 1 && (\n        adown.contains ?\n          adown.contains( bup ) :\n          a.compareDocumentPosition && a.compareDocumentPosition( bup ) & 16\n      ));\n    } :\n    function( a, b ) {\n      if ( b ) {\n        while ( (b = b.parentNode) ) {\n          if ( b === a ) {\n            return true;\n          }\n        }\n      }\n      return false;\n    };\n\n  /* Sorting\n  ---------------------------------------------------------------------- */\n\n  // Document order sorting\n  sortOrder = docElem.compareDocumentPosition ?\n  function( a, b ) {\n\n    // Flag for duplicate removal\n    if ( a === b ) {\n      hasDuplicate = true;\n      return 0;\n    }\n\n    var compare = b.compareDocumentPosition && a.compareDocumentPosition && a.compareDocumentPosition( b );\n\n    if ( compare ) {\n      // Disconnected nodes\n      if ( compare & 1 ||\n        (!support.sortDetached && b.compareDocumentPosition( a ) === compare) ) {\n\n        // Choose the first element that is related to our preferred document\n        if ( a === doc || contains(preferredDoc, a) ) {\n          return -1;\n        }\n        if ( b === doc || contains(preferredDoc, b) ) {\n          return 1;\n        }\n\n        // Maintain original order\n        return sortInput ?\n          ( indexOf.call( sortInput, a ) - indexOf.call( sortInput, b ) ) :\n          0;\n      }\n\n      return compare & 4 ? -1 : 1;\n    }\n\n    // Not directly comparable, sort on existence of method\n    return a.compareDocumentPosition ? -1 : 1;\n  } :\n  function( a, b ) {\n    var cur,\n      i = 0,\n      aup = a.parentNode,\n      bup = b.parentNode,\n      ap = [ a ],\n      bp = [ b ];\n\n    // Exit early if the nodes are identical\n    if ( a === b ) {\n      hasDuplicate = true;\n      return 0;\n\n    // Parentless nodes are either documents or disconnected\n    } else if ( !aup || !bup ) {\n      return a === doc ? -1 :\n        b === doc ? 1 :\n        aup ? -1 :\n        bup ? 1 :\n        sortInput ?\n        ( indexOf.call( sortInput, a ) - indexOf.call( sortInput, b ) ) :\n        0;\n\n    // If the nodes are siblings, we can do a quick check\n    } else if ( aup === bup ) {\n      return siblingCheck( a, b );\n    }\n\n    // Otherwise we need full lists of their ancestors for comparison\n    cur = a;\n    while ( (cur = cur.parentNode) ) {\n      ap.unshift( cur );\n    }\n    cur = b;\n    while ( (cur = cur.parentNode) ) {\n      bp.unshift( cur );\n    }\n\n    // Walk down the tree looking for a discrepancy\n    while ( ap[i] === bp[i] ) {\n      i++;\n    }\n\n    return i ?\n      // Do a sibling check if the nodes have a common ancestor\n      siblingCheck( ap[i], bp[i] ) :\n\n      // Otherwise nodes in our document sort first\n      ap[i] === preferredDoc ? -1 :\n      bp[i] === preferredDoc ? 1 :\n      0;\n  };\n\n  return doc;\n};\n\nSizzle.matches = function( expr, elements ) {\n  return Sizzle( expr, null, null, elements );\n};\n\nSizzle.matchesSelector = function( elem, expr ) {\n  // Set document vars if needed\n  if ( ( elem.ownerDocument || elem ) !== document ) {\n    setDocument( elem );\n  }\n\n  // Make sure that attribute selectors are quoted\n  expr = expr.replace( rattributeQuotes, \"='$1']\" );\n\n  if ( support.matchesSelector && documentIsHTML &&\n    ( !rbuggyMatches || !rbuggyMatches.test( expr ) ) &&\n    ( !rbuggyQSA     || !rbuggyQSA.test( expr ) ) ) {\n\n    try {\n      var ret = matches.call( elem, expr );\n\n      // IE 9's matchesSelector returns false on disconnected nodes\n      if ( ret || support.disconnectedMatch ||\n          // As well, disconnected nodes are said to be in a document\n          // fragment in IE 9\n          elem.document && elem.document.nodeType !== 11 ) {\n        return ret;\n      }\n    } catch(e) {}\n  }\n\n  return Sizzle( expr, document, null, [elem] ).length > 0;\n};\n\nSizzle.contains = function( context, elem ) {\n  // Set document vars if needed\n  if ( ( context.ownerDocument || context ) !== document ) {\n    setDocument( context );\n  }\n  return contains( context, elem );\n};\n\nSizzle.attr = function( elem, name ) {\n  // Set document vars if needed\n  if ( ( elem.ownerDocument || elem ) !== document ) {\n    setDocument( elem );\n  }\n\n  var fn = Expr.attrHandle[ name.toLowerCase() ],\n    // Don't get fooled by Object.prototype properties (jQuery #13807)\n    val = fn && hasOwn.call( Expr.attrHandle, name.toLowerCase() ) ?\n      fn( elem, name, !documentIsHTML ) :\n      undefined;\n\n  return val === undefined ?\n    support.attributes || !documentIsHTML ?\n      elem.getAttribute( name ) :\n      (val = elem.getAttributeNode(name)) && val.specified ?\n        val.value :\n        null :\n    val;\n};\n\nSizzle.error = function( msg ) {\n  throw new Error( \"Syntax error, unrecognized expression: \" + msg );\n};\n\n/**\n * Document sorting and removing duplicates\n * @param {ArrayLike} results\n */\nSizzle.uniqueSort = function( results ) {\n  var elem,\n    duplicates = [],\n    j = 0,\n    i = 0;\n\n  // Unless we *know* we can detect duplicates, assume their presence\n  hasDuplicate = !support.detectDuplicates;\n  sortInput = !support.sortStable && results.slice( 0 );\n  results.sort( sortOrder );\n\n  if ( hasDuplicate ) {\n    while ( (elem = results[i++]) ) {\n      if ( elem === results[ i ] ) {\n        j = duplicates.push( i );\n      }\n    }\n    while ( j-- ) {\n      results.splice( duplicates[ j ], 1 );\n    }\n  }\n\n  return results;\n};\n\n/**\n * Utility function for retrieving the text value of an array of DOM nodes\n * @param {Array|Element} elem\n */\ngetText = Sizzle.getText = function( elem ) {\n  var node,\n    ret = \"\",\n    i = 0,\n    nodeType = elem.nodeType;\n\n  if ( !nodeType ) {\n    // If no nodeType, this is expected to be an array\n    for ( ; (node = elem[i]); i++ ) {\n      // Do not traverse comment nodes\n      ret += getText( node );\n    }\n  } else if ( nodeType === 1 || nodeType === 9 || nodeType === 11 ) {\n    // Use textContent for elements\n    // innerText usage removed for consistency of new lines (see #11153)\n    if ( typeof elem.textContent === \"string\" ) {\n      return elem.textContent;\n    } else {\n      // Traverse its children\n      for ( elem = elem.firstChild; elem; elem = elem.nextSibling ) {\n        ret += getText( elem );\n      }\n    }\n  } else if ( nodeType === 3 || nodeType === 4 ) {\n    return elem.nodeValue;\n  }\n  // Do not include comment or processing instruction nodes\n\n  return ret;\n};\n\nExpr = Sizzle.selectors = {\n\n  // Can be adjusted by the user\n  cacheLength: 50,\n\n  createPseudo: markFunction,\n\n  match: matchExpr,\n\n  attrHandle: {},\n\n  find: {},\n\n  relative: {\n    \">\": { dir: \"parentNode\", first: true },\n    \" \": { dir: \"parentNode\" },\n    \"+\": { dir: \"previousSibling\", first: true },\n    \"~\": { dir: \"previousSibling\" }\n  },\n\n  preFilter: {\n    \"ATTR\": function( match ) {\n      match[1] = match[1].replace( runescape, funescape );\n\n      // Move the given value to match[3] whether quoted or unquoted\n      match[3] = ( match[4] || match[5] || \"\" ).replace( runescape, funescape );\n\n      if ( match[2] === \"~=\" ) {\n        match[3] = \" \" + match[3] + \" \";\n      }\n\n      return match.slice( 0, 4 );\n    },\n\n    \"CHILD\": function( match ) {\n      /* matches from matchExpr[\"CHILD\"]\n        1 type (only|nth|...)\n        2 what (child|of-type)\n        3 argument (even|odd|\\d*|\\d*n([+-]\\d+)?|...)\n        4 xn-component of xn+y argument ([+-]?\\d*n|)\n        5 sign of xn-component\n        6 x of xn-component\n        7 sign of y-component\n        8 y of y-component\n      */\n      match[1] = match[1].toLowerCase();\n\n      if ( match[1].slice( 0, 3 ) === \"nth\" ) {\n        // nth-* requires argument\n        if ( !match[3] ) {\n          Sizzle.error( match[0] );\n        }\n\n        // numeric x and y parameters for Expr.filter.CHILD\n        // remember that false/true cast respectively to 0/1\n        match[4] = +( match[4] ? match[5] + (match[6] || 1) : 2 * ( match[3] === \"even\" || match[3] === \"odd\" ) );\n        match[5] = +( ( match[7] + match[8] ) || match[3] === \"odd\" );\n\n      // other types prohibit arguments\n      } else if ( match[3] ) {\n        Sizzle.error( match[0] );\n      }\n\n      return match;\n    },\n\n    \"PSEUDO\": function( match ) {\n      var excess,\n        unquoted = !match[5] && match[2];\n\n      if ( matchExpr[\"CHILD\"].test( match[0] ) ) {\n        return null;\n      }\n\n      // Accept quoted arguments as-is\n      if ( match[3] && match[4] !== undefined ) {\n        match[2] = match[4];\n\n      // Strip excess characters from unquoted arguments\n      } else if ( unquoted && rpseudo.test( unquoted ) &&\n        // Get excess from tokenize (recursively)\n        (excess = tokenize( unquoted, true )) &&\n        // advance to the next closing parenthesis\n        (excess = unquoted.indexOf( \")\", unquoted.length - excess ) - unquoted.length) ) {\n\n        // excess is a negative index\n        match[0] = match[0].slice( 0, excess );\n        match[2] = unquoted.slice( 0, excess );\n      }\n\n      // Return only captures needed by the pseudo filter method (type and argument)\n      return match.slice( 0, 3 );\n    }\n  },\n\n  filter: {\n\n    \"TAG\": function( nodeNameSelector ) {\n      var nodeName = nodeNameSelector.replace( runescape, funescape ).toLowerCase();\n      return nodeNameSelector === \"*\" ?\n        function() { return true; } :\n        function( elem ) {\n          return elem.nodeName && elem.nodeName.toLowerCase() === nodeName;\n        };\n    },\n\n    \"CLASS\": function( className ) {\n      var pattern = classCache[ className + \" \" ];\n\n      return pattern ||\n        (pattern = new RegExp( \"(^|\" + whitespace + \")\" + className + \"(\" + whitespace + \"|$)\" )) &&\n        classCache( className, function( elem ) {\n          return pattern.test( typeof elem.className === \"string\" && elem.className || typeof elem.getAttribute !== strundefined && elem.getAttribute(\"class\") || \"\" );\n        });\n    },\n\n    \"ATTR\": function( name, operator, check ) {\n      return function( elem ) {\n        var result = Sizzle.attr( elem, name );\n\n        if ( result == null ) {\n          return operator === \"!=\";\n        }\n        if ( !operator ) {\n          return true;\n        }\n\n        result += \"\";\n\n        return operator === \"=\" ? result === check :\n          operator === \"!=\" ? result !== check :\n          operator === \"^=\" ? check && result.indexOf( check ) === 0 :\n          operator === \"*=\" ? check && result.indexOf( check ) > -1 :\n          operator === \"$=\" ? check && result.slice( -check.length ) === check :\n          operator === \"~=\" ? ( \" \" + result + \" \" ).indexOf( check ) > -1 :\n          operator === \"|=\" ? result === check || result.slice( 0, check.length + 1 ) === check + \"-\" :\n          false;\n      };\n    },\n\n    \"CHILD\": function( type, what, argument, first, last ) {\n      var simple = type.slice( 0, 3 ) !== \"nth\",\n        forward = type.slice( -4 ) !== \"last\",\n        ofType = what === \"of-type\";\n\n      return first === 1 && last === 0 ?\n\n        // Shortcut for :nth-*(n)\n        function( elem ) {\n          return !!elem.parentNode;\n        } :\n\n        function( elem, context, xml ) {\n          var cache, outerCache, node, diff, nodeIndex, start,\n            dir = simple !== forward ? \"nextSibling\" : \"previousSibling\",\n            parent = elem.parentNode,\n            name = ofType && elem.nodeName.toLowerCase(),\n            useCache = !xml && !ofType;\n\n          if ( parent ) {\n\n            // :(first|last|only)-(child|of-type)\n            if ( simple ) {\n              while ( dir ) {\n                node = elem;\n                while ( (node = node[ dir ]) ) {\n                  if ( ofType ? node.nodeName.toLowerCase() === name : node.nodeType === 1 ) {\n                    return false;\n                  }\n                }\n                // Reverse direction for :only-* (if we haven't yet done so)\n                start = dir = type === \"only\" && !start && \"nextSibling\";\n              }\n              return true;\n            }\n\n            start = [ forward ? parent.firstChild : parent.lastChild ];\n\n            // non-xml :nth-child(...) stores cache data on `parent`\n            if ( forward && useCache ) {\n              // Seek `elem` from a previously-cached index\n              outerCache = parent[ expando ] || (parent[ expando ] = {});\n              cache = outerCache[ type ] || [];\n              nodeIndex = cache[0] === dirruns && cache[1];\n              diff = cache[0] === dirruns && cache[2];\n              node = nodeIndex && parent.childNodes[ nodeIndex ];\n\n              while ( (node = ++nodeIndex && node && node[ dir ] ||\n\n                // Fallback to seeking `elem` from the start\n                (diff = nodeIndex = 0) || start.pop()) ) {\n\n                // When found, cache indexes on `parent` and break\n                if ( node.nodeType === 1 && ++diff && node === elem ) {\n                  outerCache[ type ] = [ dirruns, nodeIndex, diff ];\n                  break;\n                }\n              }\n\n            // Use previously-cached element index if available\n            } else if ( useCache && (cache = (elem[ expando ] || (elem[ expando ] = {}))[ type ]) && cache[0] === dirruns ) {\n              diff = cache[1];\n\n            // xml :nth-child(...) or :nth-last-child(...) or :nth(-last)?-of-type(...)\n            } else {\n              // Use the same loop as above to seek `elem` from the start\n              while ( (node = ++nodeIndex && node && node[ dir ] ||\n                (diff = nodeIndex = 0) || start.pop()) ) {\n\n                if ( ( ofType ? node.nodeName.toLowerCase() === name : node.nodeType === 1 ) && ++diff ) {\n                  // Cache the index of each encountered element\n                  if ( useCache ) {\n                    (node[ expando ] || (node[ expando ] = {}))[ type ] = [ dirruns, diff ];\n                  }\n\n                  if ( node === elem ) {\n                    break;\n                  }\n                }\n              }\n            }\n\n            // Incorporate the offset, then check against cycle size\n            diff -= last;\n            return diff === first || ( diff % first === 0 && diff / first >= 0 );\n          }\n        };\n    },\n\n    \"PSEUDO\": function( pseudo, argument ) {\n      // pseudo-class names are case-insensitive\n      // http://www.w3.org/TR/selectors/#pseudo-classes\n      // Prioritize by case sensitivity in case custom pseudos are added with uppercase letters\n      // Remember that setFilters inherits from pseudos\n      var args,\n        fn = Expr.pseudos[ pseudo ] || Expr.setFilters[ pseudo.toLowerCase() ] ||\n          Sizzle.error( \"unsupported pseudo: \" + pseudo );\n\n      // The user may use createPseudo to indicate that\n      // arguments are needed to create the filter function\n      // just as Sizzle does\n      if ( fn[ expando ] ) {\n        return fn( argument );\n      }\n\n      // But maintain support for old signatures\n      if ( fn.length > 1 ) {\n        args = [ pseudo, pseudo, \"\", argument ];\n        return Expr.setFilters.hasOwnProperty( pseudo.toLowerCase() ) ?\n          markFunction(function( seed, matches ) {\n            var idx,\n              matched = fn( seed, argument ),\n              i = matched.length;\n            while ( i-- ) {\n              idx = indexOf.call( seed, matched[i] );\n              seed[ idx ] = !( matches[ idx ] = matched[i] );\n            }\n          }) :\n          function( elem ) {\n            return fn( elem, 0, args );\n          };\n      }\n\n      return fn;\n    }\n  },\n\n  pseudos: {\n    // Potentially complex pseudos\n    \"not\": markFunction(function( selector ) {\n      // Trim the selector passed to compile\n      // to avoid treating leading and trailing\n      // spaces as combinators\n      var input = [],\n        results = [],\n        matcher = compile( selector.replace( rtrim, \"$1\" ) );\n\n      return matcher[ expando ] ?\n        markFunction(function( seed, matches, context, xml ) {\n          var elem,\n            unmatched = matcher( seed, null, xml, [] ),\n            i = seed.length;\n\n          // Match elements unmatched by `matcher`\n          while ( i-- ) {\n            if ( (elem = unmatched[i]) ) {\n              seed[i] = !(matches[i] = elem);\n            }\n          }\n        }) :\n        function( elem, context, xml ) {\n          input[0] = elem;\n          matcher( input, null, xml, results );\n          return !results.pop();\n        };\n    }),\n\n    \"has\": markFunction(function( selector ) {\n      return function( elem ) {\n        return Sizzle( selector, elem ).length > 0;\n      };\n    }),\n\n    \"contains\": markFunction(function( text ) {\n      return function( elem ) {\n        return ( elem.textContent || elem.innerText || getText( elem ) ).indexOf( text ) > -1;\n      };\n    }),\n\n    // \"Whether an element is represented by a :lang() selector\n    // is based solely on the element's language value\n    // being equal to the identifier C,\n    // or beginning with the identifier C immediately followed by \"-\".\n    // The matching of C against the element's language value is performed case-insensitively.\n    // The identifier C does not have to be a valid language name.\"\n    // http://www.w3.org/TR/selectors/#lang-pseudo\n    \"lang\": markFunction( function( lang ) {\n      // lang value must be a valid identifier\n      if ( !ridentifier.test(lang || \"\") ) {\n        Sizzle.error( \"unsupported lang: \" + lang );\n      }\n      lang = lang.replace( runescape, funescape ).toLowerCase();\n      return function( elem ) {\n        var elemLang;\n        do {\n          if ( (elemLang = documentIsHTML ?\n            elem.lang :\n            elem.getAttribute(\"xml:lang\") || elem.getAttribute(\"lang\")) ) {\n\n            elemLang = elemLang.toLowerCase();\n            return elemLang === lang || elemLang.indexOf( lang + \"-\" ) === 0;\n          }\n        } while ( (elem = elem.parentNode) && elem.nodeType === 1 );\n        return false;\n      };\n    }),\n\n    // Miscellaneous\n    \"target\": function( elem ) {\n      var hash = window.location && window.location.hash;\n      return hash && hash.slice( 1 ) === elem.id;\n    },\n\n    \"root\": function( elem ) {\n      return elem === docElem;\n    },\n\n    \"focus\": function( elem ) {\n      return elem === document.activeElement && (!document.hasFocus || document.hasFocus()) && !!(elem.type || elem.href || ~elem.tabIndex);\n    },\n\n    // Boolean properties\n    \"enabled\": function( elem ) {\n      return elem.disabled === false;\n    },\n\n    \"disabled\": function( elem ) {\n      return elem.disabled === true;\n    },\n\n    \"checked\": function( elem ) {\n      // In CSS3, :checked should return both checked and selected elements\n      // http://www.w3.org/TR/2011/REC-css3-selectors-20110929/#checked\n      var nodeName = elem.nodeName.toLowerCase();\n      return (nodeName === \"input\" && !!elem.checked) || (nodeName === \"option\" && !!elem.selected);\n    },\n\n    \"selected\": function( elem ) {\n      // Accessing this property makes selected-by-default\n      // options in Safari work properly\n      if ( elem.parentNode ) {\n        elem.parentNode.selectedIndex;\n      }\n\n      return elem.selected === true;\n    },\n\n    // Contents\n    \"empty\": function( elem ) {\n      // http://www.w3.org/TR/selectors/#empty-pseudo\n      // :empty is only affected by element nodes and content nodes(including text(3), cdata(4)),\n      //   not comment, processing instructions, or others\n      // Thanks to Diego Perini for the nodeName shortcut\n      //   Greater than \"@\" means alpha characters (specifically not starting with \"#\" or \"?\")\n      for ( elem = elem.firstChild; elem; elem = elem.nextSibling ) {\n        if ( elem.nodeName > \"@\" || elem.nodeType === 3 || elem.nodeType === 4 ) {\n          return false;\n        }\n      }\n      return true;\n    },\n\n    \"parent\": function( elem ) {\n      return !Expr.pseudos[\"empty\"]( elem );\n    },\n\n    // Element/input types\n    \"header\": function( elem ) {\n      return rheader.test( elem.nodeName );\n    },\n\n    \"input\": function( elem ) {\n      return rinputs.test( elem.nodeName );\n    },\n\n    \"button\": function( elem ) {\n      var name = elem.nodeName.toLowerCase();\n      return name === \"input\" && elem.type === \"button\" || name === \"button\";\n    },\n\n    \"text\": function( elem ) {\n      var attr;\n      // IE6 and 7 will map elem.type to 'text' for new HTML5 types (search, etc)\n      // use getAttribute instead to test this case\n      return elem.nodeName.toLowerCase() === \"input\" &&\n        elem.type === \"text\" &&\n        ( (attr = elem.getAttribute(\"type\")) == null || attr.toLowerCase() === elem.type );\n    },\n\n    // Position-in-collection\n    \"first\": createPositionalPseudo(function() {\n      return [ 0 ];\n    }),\n\n    \"last\": createPositionalPseudo(function( matchIndexes, length ) {\n      return [ length - 1 ];\n    }),\n\n    \"eq\": createPositionalPseudo(function( matchIndexes, length, argument ) {\n      return [ argument < 0 ? argument + length : argument ];\n    }),\n\n    \"even\": createPositionalPseudo(function( matchIndexes, length ) {\n      var i = 0;\n      for ( ; i < length; i += 2 ) {\n        matchIndexes.push( i );\n      }\n      return matchIndexes;\n    }),\n\n    \"odd\": createPositionalPseudo(function( matchIndexes, length ) {\n      var i = 1;\n      for ( ; i < length; i += 2 ) {\n        matchIndexes.push( i );\n      }\n      return matchIndexes;\n    }),\n\n    \"lt\": createPositionalPseudo(function( matchIndexes, length, argument ) {\n      var i = argument < 0 ? argument + length : argument;\n      for ( ; --i >= 0; ) {\n        matchIndexes.push( i );\n      }\n      return matchIndexes;\n    }),\n\n    \"gt\": createPositionalPseudo(function( matchIndexes, length, argument ) {\n      var i = argument < 0 ? argument + length : argument;\n      for ( ; ++i < length; ) {\n        matchIndexes.push( i );\n      }\n      return matchIndexes;\n    })\n  }\n};\n\nExpr.pseudos[\"nth\"] = Expr.pseudos[\"eq\"];\n\n// Add button/input type pseudos\nfor ( i in { radio: true, checkbox: true, file: true, password: true, image: true } ) {\n  Expr.pseudos[ i ] = createInputPseudo( i );\n}\nfor ( i in { submit: true, reset: true } ) {\n  Expr.pseudos[ i ] = createButtonPseudo( i );\n}\n\n// Easy API for creating new setFilters\nfunction setFilters() {}\nsetFilters.prototype = Expr.filters = Expr.pseudos;\nExpr.setFilters = new setFilters();\n\nfunction tokenize( selector, parseOnly ) {\n  var matched, match, tokens, type,\n    soFar, groups, preFilters,\n    cached = tokenCache[ selector + \" \" ];\n\n  if ( cached ) {\n    return parseOnly ? 0 : cached.slice( 0 );\n  }\n\n  soFar = selector;\n  groups = [];\n  preFilters = Expr.preFilter;\n\n  while ( soFar ) {\n\n    // Comma and first run\n    if ( !matched || (match = rcomma.exec( soFar )) ) {\n      if ( match ) {\n        // Don't consume trailing commas as valid\n        soFar = soFar.slice( match[0].length ) || soFar;\n      }\n      groups.push( tokens = [] );\n    }\n\n    matched = false;\n\n    // Combinators\n    if ( (match = rcombinators.exec( soFar )) ) {\n      matched = match.shift();\n      tokens.push({\n        value: matched,\n        // Cast descendant combinators to space\n        type: match[0].replace( rtrim, \" \" )\n      });\n      soFar = soFar.slice( matched.length );\n    }\n\n    // Filters\n    for ( type in Expr.filter ) {\n      if ( (match = matchExpr[ type ].exec( soFar )) && (!preFilters[ type ] ||\n        (match = preFilters[ type ]( match ))) ) {\n        matched = match.shift();\n        tokens.push({\n          value: matched,\n          type: type,\n          matches: match\n        });\n        soFar = soFar.slice( matched.length );\n      }\n    }\n\n    if ( !matched ) {\n      break;\n    }\n  }\n\n  // Return the length of the invalid excess\n  // if we're just parsing\n  // Otherwise, throw an error or return tokens\n  return parseOnly ?\n    soFar.length :\n    soFar ?\n      Sizzle.error( selector ) :\n      // Cache the tokens\n      tokenCache( selector, groups ).slice( 0 );\n}\n\nfunction toSelector( tokens ) {\n  var i = 0,\n    len = tokens.length,\n    selector = \"\";\n  for ( ; i < len; i++ ) {\n    selector += tokens[i].value;\n  }\n  return selector;\n}\n\nfunction addCombinator( matcher, combinator, base ) {\n  var dir = combinator.dir,\n    checkNonElements = base && dir === \"parentNode\",\n    doneName = done++;\n\n  return combinator.first ?\n    // Check against closest ancestor/preceding element\n    function( elem, context, xml ) {\n      while ( (elem = elem[ dir ]) ) {\n        if ( elem.nodeType === 1 || checkNonElements ) {\n          return matcher( elem, context, xml );\n        }\n      }\n    } :\n\n    // Check against all ancestor/preceding elements\n    function( elem, context, xml ) {\n      var data, cache, outerCache,\n        dirkey = dirruns + \" \" + doneName;\n\n      // We can't set arbitrary data on XML nodes, so they don't benefit from dir caching\n      if ( xml ) {\n        while ( (elem = elem[ dir ]) ) {\n          if ( elem.nodeType === 1 || checkNonElements ) {\n            if ( matcher( elem, context, xml ) ) {\n              return true;\n            }\n          }\n        }\n      } else {\n        while ( (elem = elem[ dir ]) ) {\n          if ( elem.nodeType === 1 || checkNonElements ) {\n            outerCache = elem[ expando ] || (elem[ expando ] = {});\n            if ( (cache = outerCache[ dir ]) && cache[0] === dirkey ) {\n              if ( (data = cache[1]) === true || data === cachedruns ) {\n                return data === true;\n              }\n            } else {\n              cache = outerCache[ dir ] = [ dirkey ];\n              cache[1] = matcher( elem, context, xml ) || cachedruns;\n              if ( cache[1] === true ) {\n                return true;\n              }\n            }\n          }\n        }\n      }\n    };\n}\n\nfunction elementMatcher( matchers ) {\n  return matchers.length > 1 ?\n    function( elem, context, xml ) {\n      var i = matchers.length;\n      while ( i-- ) {\n        if ( !matchers[i]( elem, context, xml ) ) {\n          return false;\n        }\n      }\n      return true;\n    } :\n    matchers[0];\n}\n\nfunction condense( unmatched, map, filter, context, xml ) {\n  var elem,\n    newUnmatched = [],\n    i = 0,\n    len = unmatched.length,\n    mapped = map != null;\n\n  for ( ; i < len; i++ ) {\n    if ( (elem = unmatched[i]) ) {\n      if ( !filter || filter( elem, context, xml ) ) {\n        newUnmatched.push( elem );\n        if ( mapped ) {\n          map.push( i );\n        }\n      }\n    }\n  }\n\n  return newUnmatched;\n}\n\nfunction setMatcher( preFilter, selector, matcher, postFilter, postFinder, postSelector ) {\n  if ( postFilter && !postFilter[ expando ] ) {\n    postFilter = setMatcher( postFilter );\n  }\n  if ( postFinder && !postFinder[ expando ] ) {\n    postFinder = setMatcher( postFinder, postSelector );\n  }\n  return markFunction(function( seed, results, context, xml ) {\n    var temp, i, elem,\n      preMap = [],\n      postMap = [],\n      preexisting = results.length,\n\n      // Get initial elements from seed or context\n      elems = seed || multipleContexts( selector || \"*\", context.nodeType ? [ context ] : context, [] ),\n\n      // Prefilter to get matcher input, preserving a map for seed-results synchronization\n      matcherIn = preFilter && ( seed || !selector ) ?\n        condense( elems, preMap, preFilter, context, xml ) :\n        elems,\n\n      matcherOut = matcher ?\n        // If we have a postFinder, or filtered seed, or non-seed postFilter or preexisting results,\n        postFinder || ( seed ? preFilter : preexisting || postFilter ) ?\n\n          // ...intermediate processing is necessary\n          [] :\n\n          // ...otherwise use results directly\n          results :\n        matcherIn;\n\n    // Find primary matches\n    if ( matcher ) {\n      matcher( matcherIn, matcherOut, context, xml );\n    }\n\n    // Apply postFilter\n    if ( postFilter ) {\n      temp = condense( matcherOut, postMap );\n      postFilter( temp, [], context, xml );\n\n      // Un-match failing elements by moving them back to matcherIn\n      i = temp.length;\n      while ( i-- ) {\n        if ( (elem = temp[i]) ) {\n          matcherOut[ postMap[i] ] = !(matcherIn[ postMap[i] ] = elem);\n        }\n      }\n    }\n\n    if ( seed ) {\n      if ( postFinder || preFilter ) {\n        if ( postFinder ) {\n          // Get the final matcherOut by condensing this intermediate into postFinder contexts\n          temp = [];\n          i = matcherOut.length;\n          while ( i-- ) {\n            if ( (elem = matcherOut[i]) ) {\n              // Restore matcherIn since elem is not yet a final match\n              temp.push( (matcherIn[i] = elem) );\n            }\n          }\n          postFinder( null, (matcherOut = []), temp, xml );\n        }\n\n        // Move matched elements from seed to results to keep them synchronized\n        i = matcherOut.length;\n        while ( i-- ) {\n          if ( (elem = matcherOut[i]) &&\n            (temp = postFinder ? indexOf.call( seed, elem ) : preMap[i]) > -1 ) {\n\n            seed[temp] = !(results[temp] = elem);\n          }\n        }\n      }\n\n    // Add elements to results, through postFinder if defined\n    } else {\n      matcherOut = condense(\n        matcherOut === results ?\n          matcherOut.splice( preexisting, matcherOut.length ) :\n          matcherOut\n      );\n      if ( postFinder ) {\n        postFinder( null, results, matcherOut, xml );\n      } else {\n        push.apply( results, matcherOut );\n      }\n    }\n  });\n}\n\nfunction matcherFromTokens( tokens ) {\n  var checkContext, matcher, j,\n    len = tokens.length,\n    leadingRelative = Expr.relative[ tokens[0].type ],\n    implicitRelative = leadingRelative || Expr.relative[\" \"],\n    i = leadingRelative ? 1 : 0,\n\n    // The foundational matcher ensures that elements are reachable from top-level context(s)\n    matchContext = addCombinator( function( elem ) {\n      return elem === checkContext;\n    }, implicitRelative, true ),\n    matchAnyContext = addCombinator( function( elem ) {\n      return indexOf.call( checkContext, elem ) > -1;\n    }, implicitRelative, true ),\n    matchers = [ function( elem, context, xml ) {\n      return ( !leadingRelative && ( xml || context !== outermostContext ) ) || (\n        (checkContext = context).nodeType ?\n          matchContext( elem, context, xml ) :\n          matchAnyContext( elem, context, xml ) );\n    } ];\n\n  for ( ; i < len; i++ ) {\n    if ( (matcher = Expr.relative[ tokens[i].type ]) ) {\n      matchers = [ addCombinator(elementMatcher( matchers ), matcher) ];\n    } else {\n      matcher = Expr.filter[ tokens[i].type ].apply( null, tokens[i].matches );\n\n      // Return special upon seeing a positional matcher\n      if ( matcher[ expando ] ) {\n        // Find the next relative operator (if any) for proper handling\n        j = ++i;\n        for ( ; j < len; j++ ) {\n          if ( Expr.relative[ tokens[j].type ] ) {\n            break;\n          }\n        }\n        return setMatcher(\n          i > 1 && elementMatcher( matchers ),\n          i > 1 && toSelector(\n            // If the preceding token was a descendant combinator, insert an implicit any-element `*`\n            tokens.slice( 0, i - 1 ).concat({ value: tokens[ i - 2 ].type === \" \" ? \"*\" : \"\" })\n          ).replace( rtrim, \"$1\" ),\n          matcher,\n          i < j && matcherFromTokens( tokens.slice( i, j ) ),\n          j < len && matcherFromTokens( (tokens = tokens.slice( j )) ),\n          j < len && toSelector( tokens )\n        );\n      }\n      matchers.push( matcher );\n    }\n  }\n\n  return elementMatcher( matchers );\n}\n\nfunction matcherFromGroupMatchers( elementMatchers, setMatchers ) {\n  // A counter to specify which element is currently being matched\n  var matcherCachedRuns = 0,\n    bySet = setMatchers.length > 0,\n    byElement = elementMatchers.length > 0,\n    superMatcher = function( seed, context, xml, results, expandContext ) {\n      var elem, j, matcher,\n        setMatched = [],\n        matchedCount = 0,\n        i = \"0\",\n        unmatched = seed && [],\n        outermost = expandContext != null,\n        contextBackup = outermostContext,\n        // We must always have either seed elements or context\n        elems = seed || byElement && Expr.find[\"TAG\"]( \"*\", expandContext && context.parentNode || context ),\n        // Use integer dirruns iff this is the outermost matcher\n        dirrunsUnique = (dirruns += contextBackup == null ? 1 : Math.random() || 0.1);\n\n      if ( outermost ) {\n        outermostContext = context !== document && context;\n        cachedruns = matcherCachedRuns;\n      }\n\n      // Add elements passing elementMatchers directly to results\n      // Keep `i` a string if there are no elements so `matchedCount` will be \"00\" below\n      for ( ; (elem = elems[i]) != null; i++ ) {\n        if ( byElement && elem ) {\n          j = 0;\n          while ( (matcher = elementMatchers[j++]) ) {\n            if ( matcher( elem, context, xml ) ) {\n              results.push( elem );\n              break;\n            }\n          }\n          if ( outermost ) {\n            dirruns = dirrunsUnique;\n            cachedruns = ++matcherCachedRuns;\n          }\n        }\n\n        // Track unmatched elements for set filters\n        if ( bySet ) {\n          // They will have gone through all possible matchers\n          if ( (elem = !matcher && elem) ) {\n            matchedCount--;\n          }\n\n          // Lengthen the array for every element, matched or not\n          if ( seed ) {\n            unmatched.push( elem );\n          }\n        }\n      }\n\n      // Apply set filters to unmatched elements\n      matchedCount += i;\n      if ( bySet && i !== matchedCount ) {\n        j = 0;\n        while ( (matcher = setMatchers[j++]) ) {\n          matcher( unmatched, setMatched, context, xml );\n        }\n\n        if ( seed ) {\n          // Reintegrate element matches to eliminate the need for sorting\n          if ( matchedCount > 0 ) {\n            while ( i-- ) {\n              if ( !(unmatched[i] || setMatched[i]) ) {\n                setMatched[i] = pop.call( results );\n              }\n            }\n          }\n\n          // Discard index placeholder values to get only actual matches\n          setMatched = condense( setMatched );\n        }\n\n        // Add matches to results\n        push.apply( results, setMatched );\n\n        // Seedless set matches succeeding multiple successful matchers stipulate sorting\n        if ( outermost && !seed && setMatched.length > 0 &&\n          ( matchedCount + setMatchers.length ) > 1 ) {\n\n          Sizzle.uniqueSort( results );\n        }\n      }\n\n      // Override manipulation of globals by nested matchers\n      if ( outermost ) {\n        dirruns = dirrunsUnique;\n        outermostContext = contextBackup;\n      }\n\n      return unmatched;\n    };\n\n  return bySet ?\n    markFunction( superMatcher ) :\n    superMatcher;\n}\n\ncompile = Sizzle.compile = function( selector, group /* Internal Use Only */ ) {\n  var i,\n    setMatchers = [],\n    elementMatchers = [],\n    cached = compilerCache[ selector + \" \" ];\n\n  if ( !cached ) {\n    // Generate a function of recursive functions that can be used to check each element\n    if ( !group ) {\n      group = tokenize( selector );\n    }\n    i = group.length;\n    while ( i-- ) {\n      cached = matcherFromTokens( group[i] );\n      if ( cached[ expando ] ) {\n        setMatchers.push( cached );\n      } else {\n        elementMatchers.push( cached );\n      }\n    }\n\n    // Cache the compiled function\n    cached = compilerCache( selector, matcherFromGroupMatchers( elementMatchers, setMatchers ) );\n  }\n  return cached;\n};\n\nfunction multipleContexts( selector, contexts, results ) {\n  var i = 0,\n    len = contexts.length;\n  for ( ; i < len; i++ ) {\n    Sizzle( selector, contexts[i], results );\n  }\n  return results;\n}\n\nfunction select( selector, context, results, seed ) {\n  var i, tokens, token, type, find,\n    match = tokenize( selector );\n\n  if ( !seed ) {\n    // Try to minimize operations if there is only one group\n    if ( match.length === 1 ) {\n\n      // Take a shortcut and set the context if the root selector is an ID\n      tokens = match[0] = match[0].slice( 0 );\n      if ( tokens.length > 2 && (token = tokens[0]).type === \"ID\" &&\n          support.getById && context.nodeType === 9 && documentIsHTML &&\n          Expr.relative[ tokens[1].type ] ) {\n\n        context = ( Expr.find[\"ID\"]( token.matches[0].replace(runescape, funescape), context ) || [] )[0];\n        if ( !context ) {\n          return results;\n        }\n        selector = selector.slice( tokens.shift().value.length );\n      }\n\n      // Fetch a seed set for right-to-left matching\n      i = matchExpr[\"needsContext\"].test( selector ) ? 0 : tokens.length;\n      while ( i-- ) {\n        token = tokens[i];\n\n        // Abort if we hit a combinator\n        if ( Expr.relative[ (type = token.type) ] ) {\n          break;\n        }\n        if ( (find = Expr.find[ type ]) ) {\n          // Search, expanding context for leading sibling combinators\n          if ( (seed = find(\n            token.matches[0].replace( runescape, funescape ),\n            rsibling.test( tokens[0].type ) && context.parentNode || context\n          )) ) {\n\n            // If seed is empty or no tokens remain, we can return early\n            tokens.splice( i, 1 );\n            selector = seed.length && toSelector( tokens );\n            if ( !selector ) {\n              push.apply( results, seed );\n              return results;\n            }\n\n            break;\n          }\n        }\n      }\n    }\n  }\n\n  // Compile and execute a filtering function\n  // Provide `match` to avoid retokenization if we modified the selector above\n  compile( selector, match )(\n    seed,\n    context,\n    !documentIsHTML,\n    results,\n    rsibling.test( selector )\n  );\n  return results;\n}\n\n// One-time assignments\n\n// Sort stability\nsupport.sortStable = expando.split(\"\").sort( sortOrder ).join(\"\") === expando;\n\n// Support: Chrome<14\n// Always assume duplicates if they aren't passed to the comparison function\nsupport.detectDuplicates = hasDuplicate;\n\n// Initialize against the default document\nsetDocument();\n\n// Support: Webkit<537.32 - Safari 6.0.3/Chrome 25 (fixed in Chrome 27)\n// Detached nodes confoundingly follow *each other*\nsupport.sortDetached = assert(function( div1 ) {\n  // Should return 1, but returns 4 (following)\n  return div1.compareDocumentPosition( document.createElement(\"div\") ) & 1;\n});\n\n// Support: IE<8\n// Prevent attribute/property \"interpolation\"\n// http://msdn.microsoft.com/en-us/library/ms536429%28VS.85%29.aspx\nif ( !assert(function( div ) {\n  div.innerHTML = \"<a href='#'></a>\";\n  return div.firstChild.getAttribute(\"href\") === \"#\" ;\n}) ) {\n  addHandle( \"type|href|height|width\", function( elem, name, isXML ) {\n    if ( !isXML ) {\n      return elem.getAttribute( name, name.toLowerCase() === \"type\" ? 1 : 2 );\n    }\n  });\n}\n\n// Support: IE<9\n// Use defaultValue in place of getAttribute(\"value\")\nif ( !support.attributes || !assert(function( div ) {\n  div.innerHTML = \"<input/>\";\n  div.firstChild.setAttribute( \"value\", \"\" );\n  return div.firstChild.getAttribute( \"value\" ) === \"\";\n}) ) {\n  addHandle( \"value\", function( elem, name, isXML ) {\n    if ( !isXML && elem.nodeName.toLowerCase() === \"input\" ) {\n      return elem.defaultValue;\n    }\n  });\n}\n\n// Support: IE<9\n// Use getAttributeNode to fetch booleans when getAttribute lies\nif ( !assert(function( div ) {\n  return div.getAttribute(\"disabled\") == null;\n}) ) {\n  addHandle( booleans, function( elem, name, isXML ) {\n    var val;\n    if ( !isXML ) {\n      return (val = elem.getAttributeNode( name )) && val.specified ?\n        val.value :\n        elem[ name ] === true ? name.toLowerCase() : null;\n    }\n  });\n}\n\njQuery.find = Sizzle;\njQuery.expr = Sizzle.selectors;\njQuery.expr[\":\"] = jQuery.expr.pseudos;\njQuery.unique = Sizzle.uniqueSort;\njQuery.text = Sizzle.getText;\njQuery.isXMLDoc = Sizzle.isXML;\njQuery.contains = Sizzle.contains;\n\n\n})( window );\n// String to Object options format cache\nvar optionsCache = {};\n\n// Convert String-formatted options into Object-formatted ones and store in cache\nfunction createOptions( options ) {\n  var object = optionsCache[ options ] = {};\n  jQuery.each( options.match( core_rnotwhite ) || [], function( _, flag ) {\n    object[ flag ] = true;\n  });\n  return object;\n}\n\n/*\n * Create a callback list using the following parameters:\n *\n *  options: an optional list of space-separated options that will change how\n *      the callback list behaves or a more traditional option object\n *\n * By default a callback list will act like an event callback list and can be\n * \"fired\" multiple times.\n *\n * Possible options:\n *\n *  once:     will ensure the callback list can only be fired once (like a Deferred)\n *\n *  memory:     will keep track of previous values and will call any callback added\n *          after the list has been fired right away with the latest \"memorized\"\n *          values (like a Deferred)\n *\n *  unique:     will ensure a callback can only be added once (no duplicate in the list)\n *\n *  stopOnFalse:  interrupt callings when a callback returns false\n *\n */\njQuery.Callbacks = function( options ) {\n\n  // Convert options from String-formatted to Object-formatted if needed\n  // (we check in cache first)\n  options = typeof options === \"string\" ?\n    ( optionsCache[ options ] || createOptions( options ) ) :\n    jQuery.extend( {}, options );\n\n  var // Last fire value (for non-forgettable lists)\n    memory,\n    // Flag to know if list was already fired\n    fired,\n    // Flag to know if list is currently firing\n    firing,\n    // First callback to fire (used internally by add and fireWith)\n    firingStart,\n    // End of the loop when firing\n    firingLength,\n    // Index of currently firing callback (modified by remove if needed)\n    firingIndex,\n    // Actual callback list\n    list = [],\n    // Stack of fire calls for repeatable lists\n    stack = !options.once && [],\n    // Fire callbacks\n    fire = function( data ) {\n      memory = options.memory && data;\n      fired = true;\n      firingIndex = firingStart || 0;\n      firingStart = 0;\n      firingLength = list.length;\n      firing = true;\n      for ( ; list && firingIndex < firingLength; firingIndex++ ) {\n        if ( list[ firingIndex ].apply( data[ 0 ], data[ 1 ] ) === false && options.stopOnFalse ) {\n          memory = false; // To prevent further calls using add\n          break;\n        }\n      }\n      firing = false;\n      if ( list ) {\n        if ( stack ) {\n          if ( stack.length ) {\n            fire( stack.shift() );\n          }\n        } else if ( memory ) {\n          list = [];\n        } else {\n          self.disable();\n        }\n      }\n    },\n    // Actual Callbacks object\n    self = {\n      // Add a callback or a collection of callbacks to the list\n      add: function() {\n        if ( list ) {\n          // First, we save the current length\n          var start = list.length;\n          (function add( args ) {\n            jQuery.each( args, function( _, arg ) {\n              var type = jQuery.type( arg );\n              if ( type === \"function\" ) {\n                if ( !options.unique || !self.has( arg ) ) {\n                  list.push( arg );\n                }\n              } else if ( arg && arg.length && type !== \"string\" ) {\n                // Inspect recursively\n                add( arg );\n              }\n            });\n          })( arguments );\n          // Do we need to add the callbacks to the\n          // current firing batch?\n          if ( firing ) {\n            firingLength = list.length;\n          // With memory, if we're not firing then\n          // we should call right away\n          } else if ( memory ) {\n            firingStart = start;\n            fire( memory );\n          }\n        }\n        return this;\n      },\n      // Remove a callback from the list\n      remove: function() {\n        if ( list ) {\n          jQuery.each( arguments, function( _, arg ) {\n            var index;\n            while( ( index = jQuery.inArray( arg, list, index ) ) > -1 ) {\n              list.splice( index, 1 );\n              // Handle firing indexes\n              if ( firing ) {\n                if ( index <= firingLength ) {\n                  firingLength--;\n                }\n                if ( index <= firingIndex ) {\n                  firingIndex--;\n                }\n              }\n            }\n          });\n        }\n        return this;\n      },\n      // Check if a given callback is in the list.\n      // If no argument is given, return whether or not list has callbacks attached.\n      has: function( fn ) {\n        return fn ? jQuery.inArray( fn, list ) > -1 : !!( list && list.length );\n      },\n      // Remove all callbacks from the list\n      empty: function() {\n        list = [];\n        firingLength = 0;\n        return this;\n      },\n      // Have the list do nothing anymore\n      disable: function() {\n        list = stack = memory = undefined;\n        return this;\n      },\n      // Is it disabled?\n      disabled: function() {\n        return !list;\n      },\n      // Lock the list in its current state\n      lock: function() {\n        stack = undefined;\n        if ( !memory ) {\n          self.disable();\n        }\n        return this;\n      },\n      // Is it locked?\n      locked: function() {\n        return !stack;\n      },\n      // Call all callbacks with the given context and arguments\n      fireWith: function( context, args ) {\n        if ( list && ( !fired || stack ) ) {\n          args = args || [];\n          args = [ context, args.slice ? args.slice() : args ];\n          if ( firing ) {\n            stack.push( args );\n          } else {\n            fire( args );\n          }\n        }\n        return this;\n      },\n      // Call all the callbacks with the given arguments\n      fire: function() {\n        self.fireWith( this, arguments );\n        return this;\n      },\n      // To know if the callbacks have already been called at least once\n      fired: function() {\n        return !!fired;\n      }\n    };\n\n  return self;\n};\njQuery.extend({\n\n  Deferred: function( func ) {\n    var tuples = [\n        // action, add listener, listener list, final state\n        [ \"resolve\", \"done\", jQuery.Callbacks(\"once memory\"), \"resolved\" ],\n        [ \"reject\", \"fail\", jQuery.Callbacks(\"once memory\"), \"rejected\" ],\n        [ \"notify\", \"progress\", jQuery.Callbacks(\"memory\") ]\n      ],\n      state = \"pending\",\n      promise = {\n        state: function() {\n          return state;\n        },\n        always: function() {\n          deferred.done( arguments ).fail( arguments );\n          return this;\n        },\n        then: function( /* fnDone, fnFail, fnProgress */ ) {\n          var fns = arguments;\n          return jQuery.Deferred(function( newDefer ) {\n            jQuery.each( tuples, function( i, tuple ) {\n              var action = tuple[ 0 ],\n                fn = jQuery.isFunction( fns[ i ] ) && fns[ i ];\n              // deferred[ done | fail | progress ] for forwarding actions to newDefer\n              deferred[ tuple[1] ](function() {\n                var returned = fn && fn.apply( this, arguments );\n                if ( returned && jQuery.isFunction( returned.promise ) ) {\n                  returned.promise()\n                    .done( newDefer.resolve )\n                    .fail( newDefer.reject )\n                    .progress( newDefer.notify );\n                } else {\n                  newDefer[ action + \"With\" ]( this === promise ? newDefer.promise() : this, fn ? [ returned ] : arguments );\n                }\n              });\n            });\n            fns = null;\n          }).promise();\n        },\n        // Get a promise for this deferred\n        // If obj is provided, the promise aspect is added to the object\n        promise: function( obj ) {\n          return obj != null ? jQuery.extend( obj, promise ) : promise;\n        }\n      },\n      deferred = {};\n\n    // Keep pipe for back-compat\n    promise.pipe = promise.then;\n\n    // Add list-specific methods\n    jQuery.each( tuples, function( i, tuple ) {\n      var list = tuple[ 2 ],\n        stateString = tuple[ 3 ];\n\n      // promise[ done | fail | progress ] = list.add\n      promise[ tuple[1] ] = list.add;\n\n      // Handle state\n      if ( stateString ) {\n        list.add(function() {\n          // state = [ resolved | rejected ]\n          state = stateString;\n\n        // [ reject_list | resolve_list ].disable; progress_list.lock\n        }, tuples[ i ^ 1 ][ 2 ].disable, tuples[ 2 ][ 2 ].lock );\n      }\n\n      // deferred[ resolve | reject | notify ]\n      deferred[ tuple[0] ] = function() {\n        deferred[ tuple[0] + \"With\" ]( this === deferred ? promise : this, arguments );\n        return this;\n      };\n      deferred[ tuple[0] + \"With\" ] = list.fireWith;\n    });\n\n    // Make the deferred a promise\n    promise.promise( deferred );\n\n    // Call given func if any\n    if ( func ) {\n      func.call( deferred, deferred );\n    }\n\n    // All done!\n    return deferred;\n  },\n\n  // Deferred helper\n  when: function( subordinate /* , ..., subordinateN */ ) {\n    var i = 0,\n      resolveValues = core_slice.call( arguments ),\n      length = resolveValues.length,\n\n      // the count of uncompleted subordinates\n      remaining = length !== 1 || ( subordinate && jQuery.isFunction( subordinate.promise ) ) ? length : 0,\n\n      // the master Deferred. If resolveValues consist of only a single Deferred, just use that.\n      deferred = remaining === 1 ? subordinate : jQuery.Deferred(),\n\n      // Update function for both resolve and progress values\n      updateFunc = function( i, contexts, values ) {\n        return function( value ) {\n          contexts[ i ] = this;\n          values[ i ] = arguments.length > 1 ? core_slice.call( arguments ) : value;\n          if( values === progressValues ) {\n            deferred.notifyWith( contexts, values );\n          } else if ( !( --remaining ) ) {\n            deferred.resolveWith( contexts, values );\n          }\n        };\n      },\n\n      progressValues, progressContexts, resolveContexts;\n\n    // add listeners to Deferred subordinates; treat others as resolved\n    if ( length > 1 ) {\n      progressValues = new Array( length );\n      progressContexts = new Array( length );\n      resolveContexts = new Array( length );\n      for ( ; i < length; i++ ) {\n        if ( resolveValues[ i ] && jQuery.isFunction( resolveValues[ i ].promise ) ) {\n          resolveValues[ i ].promise()\n            .done( updateFunc( i, resolveContexts, resolveValues ) )\n            .fail( deferred.reject )\n            .progress( updateFunc( i, progressContexts, progressValues ) );\n        } else {\n          --remaining;\n        }\n      }\n    }\n\n    // if we're not waiting on anything, resolve the master\n    if ( !remaining ) {\n      deferred.resolveWith( resolveContexts, resolveValues );\n    }\n\n    return deferred.promise();\n  }\n});\njQuery.support = (function( support ) {\n  var input = document.createElement(\"input\"),\n    fragment = document.createDocumentFragment(),\n    div = document.createElement(\"div\"),\n    select = document.createElement(\"select\"),\n    opt = select.appendChild( document.createElement(\"option\") );\n\n  // Finish early in limited environments\n  if ( !input.type ) {\n    return support;\n  }\n\n  input.type = \"checkbox\";\n\n  // Support: Safari 5.1, iOS 5.1, Android 4.x, Android 2.3\n  // Check the default checkbox/radio value (\"\" on old WebKit; \"on\" elsewhere)\n  support.checkOn = input.value !== \"\";\n\n  // Must access the parent to make an option select properly\n  // Support: IE9, IE10\n  support.optSelected = opt.selected;\n\n  // Will be defined later\n  support.reliableMarginRight = true;\n  support.boxSizingReliable = true;\n  support.pixelPosition = false;\n\n  // Make sure checked status is properly cloned\n  // Support: IE9, IE10\n  input.checked = true;\n  support.noCloneChecked = input.cloneNode( true ).checked;\n\n  // Make sure that the options inside disabled selects aren't marked as disabled\n  // (WebKit marks them as disabled)\n  select.disabled = true;\n  support.optDisabled = !opt.disabled;\n\n  // Check if an input maintains its value after becoming a radio\n  // Support: IE9, IE10\n  input = document.createElement(\"input\");\n  input.value = \"t\";\n  input.type = \"radio\";\n  support.radioValue = input.value === \"t\";\n\n  // #11217 - WebKit loses check when the name is after the checked attribute\n  input.setAttribute( \"checked\", \"t\" );\n  input.setAttribute( \"name\", \"t\" );\n\n  fragment.appendChild( input );\n\n  // Support: Safari 5.1, Android 4.x, Android 2.3\n  // old WebKit doesn't clone checked state correctly in fragments\n  support.checkClone = fragment.cloneNode( true ).cloneNode( true ).lastChild.checked;\n\n  // Support: Firefox, Chrome, Safari\n  // Beware of CSP restrictions (https://developer.mozilla.org/en/Security/CSP)\n  support.focusinBubbles = \"onfocusin\" in window;\n\n  div.style.backgroundClip = \"content-box\";\n  div.cloneNode( true ).style.backgroundClip = \"\";\n  support.clearCloneStyle = div.style.backgroundClip === \"content-box\";\n\n  // Run tests that need a body at doc ready\n  jQuery(function() {\n    var container, marginDiv,\n      // Support: Firefox, Android 2.3 (Prefixed box-sizing versions).\n      divReset = \"padding:0;margin:0;border:0;display:block;-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box\",\n      body = document.getElementsByTagName(\"body\")[ 0 ];\n\n    if ( !body ) {\n      // Return for frameset docs that don't have a body\n      return;\n    }\n\n    container = document.createElement(\"div\");\n    container.style.cssText = \"border:0;width:0;height:0;position:absolute;top:0;left:-9999px;margin-top:1px\";\n\n    // Check box-sizing and margin behavior.\n    body.appendChild( container ).appendChild( div );\n    div.innerHTML = \"\";\n    // Support: Firefox, Android 2.3 (Prefixed box-sizing versions).\n    div.style.cssText = \"-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;padding:1px;border:1px;display:block;width:4px;margin-top:1%;position:absolute;top:1%\";\n\n    // Workaround failing boxSizing test due to offsetWidth returning wrong value\n    // with some non-1 values of body zoom, ticket #13543\n    jQuery.swap( body, body.style.zoom != null ? { zoom: 1 } : {}, function() {\n      support.boxSizing = div.offsetWidth === 4;\n    });\n\n    // Use window.getComputedStyle because jsdom on node.js will break without it.\n    if ( window.getComputedStyle ) {\n      support.pixelPosition = ( window.getComputedStyle( div, null ) || {} ).top !== \"1%\";\n      support.boxSizingReliable = ( window.getComputedStyle( div, null ) || { width: \"4px\" } ).width === \"4px\";\n\n      // Support: Android 2.3\n      // Check if div with explicit width and no margin-right incorrectly\n      // gets computed margin-right based on width of container. (#3333)\n      // WebKit Bug 13343 - getComputedStyle returns wrong value for margin-right\n      marginDiv = div.appendChild( document.createElement(\"div\") );\n      marginDiv.style.cssText = div.style.cssText = divReset;\n      marginDiv.style.marginRight = marginDiv.style.width = \"0\";\n      div.style.width = \"1px\";\n\n      support.reliableMarginRight =\n        !parseFloat( ( window.getComputedStyle( marginDiv, null ) || {} ).marginRight );\n    }\n\n    body.removeChild( container );\n  });\n\n  return support;\n})( {} );\n\n/*\n  Implementation Summary\n\n  1. Enforce API surface and semantic compatibility with 1.9.x branch\n  2. Improve the module's maintainability by reducing the storage\n    paths to a single mechanism.\n  3. Use the same single mechanism to support \"private\" and \"user\" data.\n  4. _Never_ expose \"private\" data to user code (TODO: Drop _data, _removeData)\n  5. Avoid exposing implementation details on user objects (eg. expando properties)\n  6. Provide a clear path for implementation upgrade to WeakMap in 2014\n*/\nvar data_user, data_priv,\n  rbrace = /(?:\\{[\\s\\S]*\\}|\\[[\\s\\S]*\\])$/,\n  rmultiDash = /([A-Z])/g;\n\nfunction Data() {\n  // Support: Android < 4,\n  // Old WebKit does not have Object.preventExtensions/freeze method,\n  // return new empty object instead with no [[set]] accessor\n  Object.defineProperty( this.cache = {}, 0, {\n    get: function() {\n      return {};\n    }\n  });\n\n  this.expando = jQuery.expando + Math.random();\n}\n\nData.uid = 1;\n\nData.accepts = function( owner ) {\n  // Accepts only:\n  //  - Node\n  //    - Node.ELEMENT_NODE\n  //    - Node.DOCUMENT_NODE\n  //  - Object\n  //    - Any\n  return owner.nodeType ?\n    owner.nodeType === 1 || owner.nodeType === 9 : true;\n};\n\nData.prototype = {\n  key: function( owner ) {\n    // We can accept data for non-element nodes in modern browsers,\n    // but we should not, see #8335.\n    // Always return the key for a frozen object.\n    if ( !Data.accepts( owner ) ) {\n      return 0;\n    }\n\n    var descriptor = {},\n      // Check if the owner object already has a cache key\n      unlock = owner[ this.expando ];\n\n    // If not, create one\n    if ( !unlock ) {\n      unlock = Data.uid++;\n\n      // Secure it in a non-enumerable, non-writable property\n      try {\n        descriptor[ this.expando ] = { value: unlock };\n        Object.defineProperties( owner, descriptor );\n\n      // Support: Android < 4\n      // Fallback to a less secure definition\n      } catch ( e ) {\n        descriptor[ this.expando ] = unlock;\n        jQuery.extend( owner, descriptor );\n      }\n    }\n\n    // Ensure the cache object\n    if ( !this.cache[ unlock ] ) {\n      this.cache[ unlock ] = {};\n    }\n\n    return unlock;\n  },\n  set: function( owner, data, value ) {\n    var prop,\n      // There may be an unlock assigned to this node,\n      // if there is no entry for this \"owner\", create one inline\n      // and set the unlock as though an owner entry had always existed\n      unlock = this.key( owner ),\n      cache = this.cache[ unlock ];\n\n    // Handle: [ owner, key, value ] args\n    if ( typeof data === \"string\" ) {\n      cache[ data ] = value;\n\n    // Handle: [ owner, { properties } ] args\n    } else {\n      // Fresh assignments by object are shallow copied\n      if ( jQuery.isEmptyObject( cache ) ) {\n        jQuery.extend( this.cache[ unlock ], data );\n      // Otherwise, copy the properties one-by-one to the cache object\n      } else {\n        for ( prop in data ) {\n          cache[ prop ] = data[ prop ];\n        }\n      }\n    }\n    return cache;\n  },\n  get: function( owner, key ) {\n    // Either a valid cache is found, or will be created.\n    // New caches will be created and the unlock returned,\n    // allowing direct access to the newly created\n    // empty data object. A valid owner object must be provided.\n    var cache = this.cache[ this.key( owner ) ];\n\n    return key === undefined ?\n      cache : cache[ key ];\n  },\n  access: function( owner, key, value ) {\n    var stored;\n    // In cases where either:\n    //\n    //   1. No key was specified\n    //   2. A string key was specified, but no value provided\n    //\n    // Take the \"read\" path and allow the get method to determine\n    // which value to return, respectively either:\n    //\n    //   1. The entire cache object\n    //   2. The data stored at the key\n    //\n    if ( key === undefined ||\n        ((key && typeof key === \"string\") && value === undefined) ) {\n\n      stored = this.get( owner, key );\n\n      return stored !== undefined ?\n        stored : this.get( owner, jQuery.camelCase(key) );\n    }\n\n    // [*]When the key is not a string, or both a key and value\n    // are specified, set or extend (existing objects) with either:\n    //\n    //   1. An object of properties\n    //   2. A key and value\n    //\n    this.set( owner, key, value );\n\n    // Since the \"set\" path can have two possible entry points\n    // return the expected data based on which path was taken[*]\n    return value !== undefined ? value : key;\n  },\n  remove: function( owner, key ) {\n    var i, name, camel,\n      unlock = this.key( owner ),\n      cache = this.cache[ unlock ];\n\n    if ( key === undefined ) {\n      this.cache[ unlock ] = {};\n\n    } else {\n      // Support array or space separated string of keys\n      if ( jQuery.isArray( key ) ) {\n        // If \"name\" is an array of keys...\n        // When data is initially created, via (\"key\", \"val\") signature,\n        // keys will be converted to camelCase.\n        // Since there is no way to tell _how_ a key was added, remove\n        // both plain key and camelCase key. #12786\n        // This will only penalize the array argument path.\n        name = key.concat( key.map( jQuery.camelCase ) );\n      } else {\n        camel = jQuery.camelCase( key );\n        // Try the string as a key before any manipulation\n        if ( key in cache ) {\n          name = [ key, camel ];\n        } else {\n          // If a key with the spaces exists, use it.\n          // Otherwise, create an array by matching non-whitespace\n          name = camel;\n          name = name in cache ?\n            [ name ] : ( name.match( core_rnotwhite ) || [] );\n        }\n      }\n\n      i = name.length;\n      while ( i-- ) {\n        delete cache[ name[ i ] ];\n      }\n    }\n  },\n  hasData: function( owner ) {\n    return !jQuery.isEmptyObject(\n      this.cache[ owner[ this.expando ] ] || {}\n    );\n  },\n  discard: function( owner ) {\n    if ( owner[ this.expando ] ) {\n      delete this.cache[ owner[ this.expando ] ];\n    }\n  }\n};\n\n// These may be used throughout the jQuery core codebase\ndata_user = new Data();\ndata_priv = new Data();\n\n\njQuery.extend({\n  acceptData: Data.accepts,\n\n  hasData: function( elem ) {\n    return data_user.hasData( elem ) || data_priv.hasData( elem );\n  },\n\n  data: function( elem, name, data ) {\n    return data_user.access( elem, name, data );\n  },\n\n  removeData: function( elem, name ) {\n    data_user.remove( elem, name );\n  },\n\n  // TODO: Now that all calls to _data and _removeData have been replaced\n  // with direct calls to data_priv methods, these can be deprecated.\n  _data: function( elem, name, data ) {\n    return data_priv.access( elem, name, data );\n  },\n\n  _removeData: function( elem, name ) {\n    data_priv.remove( elem, name );\n  }\n});\n\njQuery.fn.extend({\n  data: function( key, value ) {\n    var attrs, name,\n      elem = this[ 0 ],\n      i = 0,\n      data = null;\n\n    // Gets all values\n    if ( key === undefined ) {\n      if ( this.length ) {\n        data = data_user.get( elem );\n\n        if ( elem.nodeType === 1 && !data_priv.get( elem, \"hasDataAttrs\" ) ) {\n          attrs = elem.attributes;\n          for ( ; i < attrs.length; i++ ) {\n            name = attrs[ i ].name;\n\n            if ( name.indexOf( \"data-\" ) === 0 ) {\n              name = jQuery.camelCase( name.slice(5) );\n              dataAttr( elem, name, data[ name ] );\n            }\n          }\n          data_priv.set( elem, \"hasDataAttrs\", true );\n        }\n      }\n\n      return data;\n    }\n\n    // Sets multiple values\n    if ( typeof key === \"object\" ) {\n      return this.each(function() {\n        data_user.set( this, key );\n      });\n    }\n\n    return jQuery.access( this, function( value ) {\n      var data,\n        camelKey = jQuery.camelCase( key );\n\n      // The calling jQuery object (element matches) is not empty\n      // (and therefore has an element appears at this[ 0 ]) and the\n      // `value` parameter was not undefined. An empty jQuery object\n      // will result in `undefined` for elem = this[ 0 ] which will\n      // throw an exception if an attempt to read a data cache is made.\n      if ( elem && value === undefined ) {\n        // Attempt to get data from the cache\n        // with the key as-is\n        data = data_user.get( elem, key );\n        if ( data !== undefined ) {\n          return data;\n        }\n\n        // Attempt to get data from the cache\n        // with the key camelized\n        data = data_user.get( elem, camelKey );\n        if ( data !== undefined ) {\n          return data;\n        }\n\n        // Attempt to \"discover\" the data in\n        // HTML5 custom data-* attrs\n        data = dataAttr( elem, camelKey, undefined );\n        if ( data !== undefined ) {\n          return data;\n        }\n\n        // We tried really hard, but the data doesn't exist.\n        return;\n      }\n\n      // Set the data...\n      this.each(function() {\n        // First, attempt to store a copy or reference of any\n        // data that might've been store with a camelCased key.\n        var data = data_user.get( this, camelKey );\n\n        // For HTML5 data-* attribute interop, we have to\n        // store property names with dashes in a camelCase form.\n        // This might not apply to all properties...*\n        data_user.set( this, camelKey, value );\n\n        // *... In the case of properties that might _actually_\n        // have dashes, we need to also store a copy of that\n        // unchanged property.\n        if ( key.indexOf(\"-\") !== -1 && data !== undefined ) {\n          data_user.set( this, key, value );\n        }\n      });\n    }, null, value, arguments.length > 1, null, true );\n  },\n\n  removeData: function( key ) {\n    return this.each(function() {\n      data_user.remove( this, key );\n    });\n  }\n});\n\nfunction dataAttr( elem, key, data ) {\n  var name;\n\n  // If nothing was found internally, try to fetch any\n  // data from the HTML5 data-* attribute\n  if ( data === undefined && elem.nodeType === 1 ) {\n    name = \"data-\" + key.replace( rmultiDash, \"-$1\" ).toLowerCase();\n    data = elem.getAttribute( name );\n\n    if ( typeof data === \"string\" ) {\n      try {\n        data = data === \"true\" ? true :\n          data === \"false\" ? false :\n          data === \"null\" ? null :\n          // Only convert to a number if it doesn't change the string\n          +data + \"\" === data ? +data :\n          rbrace.test( data ) ? JSON.parse( data ) :\n          data;\n      } catch( e ) {}\n\n      // Make sure we set the data so it isn't changed later\n      data_user.set( elem, key, data );\n    } else {\n      data = undefined;\n    }\n  }\n  return data;\n}\njQuery.extend({\n  queue: function( elem, type, data ) {\n    var queue;\n\n    if ( elem ) {\n      type = ( type || \"fx\" ) + \"queue\";\n      queue = data_priv.get( elem, type );\n\n      // Speed up dequeue by getting out quickly if this is just a lookup\n      if ( data ) {\n        if ( !queue || jQuery.isArray( data ) ) {\n          queue = data_priv.access( elem, type, jQuery.makeArray(data) );\n        } else {\n          queue.push( data );\n        }\n      }\n      return queue || [];\n    }\n  },\n\n  dequeue: function( elem, type ) {\n    type = type || \"fx\";\n\n    var queue = jQuery.queue( elem, type ),\n      startLength = queue.length,\n      fn = queue.shift(),\n      hooks = jQuery._queueHooks( elem, type ),\n      next = function() {\n        jQuery.dequeue( elem, type );\n      };\n\n    // If the fx queue is dequeued, always remove the progress sentinel\n    if ( fn === \"inprogress\" ) {\n      fn = queue.shift();\n      startLength--;\n    }\n\n    if ( fn ) {\n\n      // Add a progress sentinel to prevent the fx queue from being\n      // automatically dequeued\n      if ( type === \"fx\" ) {\n        queue.unshift( \"inprogress\" );\n      }\n\n      // clear up the last queue stop function\n      delete hooks.stop;\n      fn.call( elem, next, hooks );\n    }\n\n    if ( !startLength && hooks ) {\n      hooks.empty.fire();\n    }\n  },\n\n  // not intended for public consumption - generates a queueHooks object, or returns the current one\n  _queueHooks: function( elem, type ) {\n    var key = type + \"queueHooks\";\n    return data_priv.get( elem, key ) || data_priv.access( elem, key, {\n      empty: jQuery.Callbacks(\"once memory\").add(function() {\n        data_priv.remove( elem, [ type + \"queue\", key ] );\n      })\n    });\n  }\n});\n\njQuery.fn.extend({\n  queue: function( type, data ) {\n    var setter = 2;\n\n    if ( typeof type !== \"string\" ) {\n      data = type;\n      type = \"fx\";\n      setter--;\n    }\n\n    if ( arguments.length < setter ) {\n      return jQuery.queue( this[0], type );\n    }\n\n    return data === undefined ?\n      this :\n      this.each(function() {\n        var queue = jQuery.queue( this, type, data );\n\n        // ensure a hooks for this queue\n        jQuery._queueHooks( this, type );\n\n        if ( type === \"fx\" && queue[0] !== \"inprogress\" ) {\n          jQuery.dequeue( this, type );\n        }\n      });\n  },\n  dequeue: function( type ) {\n    return this.each(function() {\n      jQuery.dequeue( this, type );\n    });\n  },\n  // Based off of the plugin by Clint Helfers, with permission.\n  // http://blindsignals.com/index.php/2009/07/jquery-delay/\n  delay: function( time, type ) {\n    time = jQuery.fx ? jQuery.fx.speeds[ time ] || time : time;\n    type = type || \"fx\";\n\n    return this.queue( type, function( next, hooks ) {\n      var timeout = setTimeout( next, time );\n      hooks.stop = function() {\n        clearTimeout( timeout );\n      };\n    });\n  },\n  clearQueue: function( type ) {\n    return this.queue( type || \"fx\", [] );\n  },\n  // Get a promise resolved when queues of a certain type\n  // are emptied (fx is the type by default)\n  promise: function( type, obj ) {\n    var tmp,\n      count = 1,\n      defer = jQuery.Deferred(),\n      elements = this,\n      i = this.length,\n      resolve = function() {\n        if ( !( --count ) ) {\n          defer.resolveWith( elements, [ elements ] );\n        }\n      };\n\n    if ( typeof type !== \"string\" ) {\n      obj = type;\n      type = undefined;\n    }\n    type = type || \"fx\";\n\n    while( i-- ) {\n      tmp = data_priv.get( elements[ i ], type + \"queueHooks\" );\n      if ( tmp && tmp.empty ) {\n        count++;\n        tmp.empty.add( resolve );\n      }\n    }\n    resolve();\n    return defer.promise( obj );\n  }\n});\nvar nodeHook, boolHook,\n  rclass = /[\\t\\r\\n\\f]/g,\n  rreturn = /\\r/g,\n  rfocusable = /^(?:input|select|textarea|button)$/i;\n\njQuery.fn.extend({\n  attr: function( name, value ) {\n    return jQuery.access( this, jQuery.attr, name, value, arguments.length > 1 );\n  },\n\n  removeAttr: function( name ) {\n    return this.each(function() {\n      jQuery.removeAttr( this, name );\n    });\n  },\n\n  prop: function( name, value ) {\n    return jQuery.access( this, jQuery.prop, name, value, arguments.length > 1 );\n  },\n\n  removeProp: function( name ) {\n    return this.each(function() {\n      delete this[ jQuery.propFix[ name ] || name ];\n    });\n  },\n\n  addClass: function( value ) {\n    var classes, elem, cur, clazz, j,\n      i = 0,\n      len = this.length,\n      proceed = typeof value === \"string\" && value;\n\n    if ( jQuery.isFunction( value ) ) {\n      return this.each(function( j ) {\n        jQuery( this ).addClass( value.call( this, j, this.className ) );\n      });\n    }\n\n    if ( proceed ) {\n      // The disjunction here is for better compressibility (see removeClass)\n      classes = ( value || \"\" ).match( core_rnotwhite ) || [];\n\n      for ( ; i < len; i++ ) {\n        elem = this[ i ];\n        cur = elem.nodeType === 1 && ( elem.className ?\n          ( \" \" + elem.className + \" \" ).replace( rclass, \" \" ) :\n          \" \"\n        );\n\n        if ( cur ) {\n          j = 0;\n          while ( (clazz = classes[j++]) ) {\n            if ( cur.indexOf( \" \" + clazz + \" \" ) < 0 ) {\n              cur += clazz + \" \";\n            }\n          }\n          elem.className = jQuery.trim( cur );\n\n        }\n      }\n    }\n\n    return this;\n  },\n\n  removeClass: function( value ) {\n    var classes, elem, cur, clazz, j,\n      i = 0,\n      len = this.length,\n      proceed = arguments.length === 0 || typeof value === \"string\" && value;\n\n    if ( jQuery.isFunction( value ) ) {\n      return this.each(function( j ) {\n        jQuery( this ).removeClass( value.call( this, j, this.className ) );\n      });\n    }\n    if ( proceed ) {\n      classes = ( value || \"\" ).match( core_rnotwhite ) || [];\n\n      for ( ; i < len; i++ ) {\n        elem = this[ i ];\n        // This expression is here for better compressibility (see addClass)\n        cur = elem.nodeType === 1 && ( elem.className ?\n          ( \" \" + elem.className + \" \" ).replace( rclass, \" \" ) :\n          \"\"\n        );\n\n        if ( cur ) {\n          j = 0;\n          while ( (clazz = classes[j++]) ) {\n            // Remove *all* instances\n            while ( cur.indexOf( \" \" + clazz + \" \" ) >= 0 ) {\n              cur = cur.replace( \" \" + clazz + \" \", \" \" );\n            }\n          }\n          elem.className = value ? jQuery.trim( cur ) : \"\";\n        }\n      }\n    }\n\n    return this;\n  },\n\n  toggleClass: function( value, stateVal ) {\n    var type = typeof value;\n\n    if ( typeof stateVal === \"boolean\" && type === \"string\" ) {\n      return stateVal ? this.addClass( value ) : this.removeClass( value );\n    }\n\n    if ( jQuery.isFunction( value ) ) {\n      return this.each(function( i ) {\n        jQuery( this ).toggleClass( value.call(this, i, this.className, stateVal), stateVal );\n      });\n    }\n\n    return this.each(function() {\n      if ( type === \"string\" ) {\n        // toggle individual class names\n        var className,\n          i = 0,\n          self = jQuery( this ),\n          classNames = value.match( core_rnotwhite ) || [];\n\n        while ( (className = classNames[ i++ ]) ) {\n          // check each className given, space separated list\n          if ( self.hasClass( className ) ) {\n            self.removeClass( className );\n          } else {\n            self.addClass( className );\n          }\n        }\n\n      // Toggle whole class name\n      } else if ( type === core_strundefined || type === \"boolean\" ) {\n        if ( this.className ) {\n          // store className if set\n          data_priv.set( this, \"__className__\", this.className );\n        }\n\n        // If the element has a class name or if we're passed \"false\",\n        // then remove the whole classname (if there was one, the above saved it).\n        // Otherwise bring back whatever was previously saved (if anything),\n        // falling back to the empty string if nothing was stored.\n        this.className = this.className || value === false ? \"\" : data_priv.get( this, \"__className__\" ) || \"\";\n      }\n    });\n  },\n\n  hasClass: function( selector ) {\n    var className = \" \" + selector + \" \",\n      i = 0,\n      l = this.length;\n    for ( ; i < l; i++ ) {\n      if ( this[i].nodeType === 1 && (\" \" + this[i].className + \" \").replace(rclass, \" \").indexOf( className ) >= 0 ) {\n        return true;\n      }\n    }\n\n    return false;\n  },\n\n  val: function( value ) {\n    var hooks, ret, isFunction,\n      elem = this[0];\n\n    if ( !arguments.length ) {\n      if ( elem ) {\n        hooks = jQuery.valHooks[ elem.type ] || jQuery.valHooks[ elem.nodeName.toLowerCase() ];\n\n        if ( hooks && \"get\" in hooks && (ret = hooks.get( elem, \"value\" )) !== undefined ) {\n          return ret;\n        }\n\n        ret = elem.value;\n\n        return typeof ret === \"string\" ?\n          // handle most common string cases\n          ret.replace(rreturn, \"\") :\n          // handle cases where value is null/undef or number\n          ret == null ? \"\" : ret;\n      }\n\n      return;\n    }\n\n    isFunction = jQuery.isFunction( value );\n\n    return this.each(function( i ) {\n      var val;\n\n      if ( this.nodeType !== 1 ) {\n        return;\n      }\n\n      if ( isFunction ) {\n        val = value.call( this, i, jQuery( this ).val() );\n      } else {\n        val = value;\n      }\n\n      // Treat null/undefined as \"\"; convert numbers to string\n      if ( val == null ) {\n        val = \"\";\n      } else if ( typeof val === \"number\" ) {\n        val += \"\";\n      } else if ( jQuery.isArray( val ) ) {\n        val = jQuery.map(val, function ( value ) {\n          return value == null ? \"\" : value + \"\";\n        });\n      }\n\n      hooks = jQuery.valHooks[ this.type ] || jQuery.valHooks[ this.nodeName.toLowerCase() ];\n\n      // If set returns undefined, fall back to normal setting\n      if ( !hooks || !(\"set\" in hooks) || hooks.set( this, val, \"value\" ) === undefined ) {\n        this.value = val;\n      }\n    });\n  }\n});\n\njQuery.extend({\n  valHooks: {\n    option: {\n      get: function( elem ) {\n        // attributes.value is undefined in Blackberry 4.7 but\n        // uses .value. See #6932\n        var val = elem.attributes.value;\n        return !val || val.specified ? elem.value : elem.text;\n      }\n    },\n    select: {\n      get: function( elem ) {\n        var value, option,\n          options = elem.options,\n          index = elem.selectedIndex,\n          one = elem.type === \"select-one\" || index < 0,\n          values = one ? null : [],\n          max = one ? index + 1 : options.length,\n          i = index < 0 ?\n            max :\n            one ? index : 0;\n\n        // Loop through all the selected options\n        for ( ; i < max; i++ ) {\n          option = options[ i ];\n\n          // IE6-9 doesn't update selected after form reset (#2551)\n          if ( ( option.selected || i === index ) &&\n              // Don't return options that are disabled or in a disabled optgroup\n              ( jQuery.support.optDisabled ? !option.disabled : option.getAttribute(\"disabled\") === null ) &&\n              ( !option.parentNode.disabled || !jQuery.nodeName( option.parentNode, \"optgroup\" ) ) ) {\n\n            // Get the specific value for the option\n            value = jQuery( option ).val();\n\n            // We don't need an array for one selects\n            if ( one ) {\n              return value;\n            }\n\n            // Multi-Selects return an array\n            values.push( value );\n          }\n        }\n\n        return values;\n      },\n\n      set: function( elem, value ) {\n        var optionSet, option,\n          options = elem.options,\n          values = jQuery.makeArray( value ),\n          i = options.length;\n\n        while ( i-- ) {\n          option = options[ i ];\n          if ( (option.selected = jQuery.inArray( jQuery(option).val(), values ) >= 0) ) {\n            optionSet = true;\n          }\n        }\n\n        // force browsers to behave consistently when non-matching value is set\n        if ( !optionSet ) {\n          elem.selectedIndex = -1;\n        }\n        return values;\n      }\n    }\n  },\n\n  attr: function( elem, name, value ) {\n    var hooks, ret,\n      nType = elem.nodeType;\n\n    // don't get/set attributes on text, comment and attribute nodes\n    if ( !elem || nType === 3 || nType === 8 || nType === 2 ) {\n      return;\n    }\n\n    // Fallback to prop when attributes are not supported\n    if ( typeof elem.getAttribute === core_strundefined ) {\n      return jQuery.prop( elem, name, value );\n    }\n\n    // All attributes are lowercase\n    // Grab necessary hook if one is defined\n    if ( nType !== 1 || !jQuery.isXMLDoc( elem ) ) {\n      name = name.toLowerCase();\n      hooks = jQuery.attrHooks[ name ] ||\n        ( jQuery.expr.match.bool.test( name ) ? boolHook : nodeHook );\n    }\n\n    if ( value !== undefined ) {\n\n      if ( value === null ) {\n        jQuery.removeAttr( elem, name );\n\n      } else if ( hooks && \"set\" in hooks && (ret = hooks.set( elem, value, name )) !== undefined ) {\n        return ret;\n\n      } else {\n        elem.setAttribute( name, value + \"\" );\n        return value;\n      }\n\n    } else if ( hooks && \"get\" in hooks && (ret = hooks.get( elem, name )) !== null ) {\n      return ret;\n\n    } else {\n      ret = jQuery.find.attr( elem, name );\n\n      // Non-existent attributes return null, we normalize to undefined\n      return ret == null ?\n        undefined :\n        ret;\n    }\n  },\n\n  removeAttr: function( elem, value ) {\n    var name, propName,\n      i = 0,\n      attrNames = value && value.match( core_rnotwhite );\n\n    if ( attrNames && elem.nodeType === 1 ) {\n      while ( (name = attrNames[i++]) ) {\n        propName = jQuery.propFix[ name ] || name;\n\n        // Boolean attributes get special treatment (#10870)\n        if ( jQuery.expr.match.bool.test( name ) ) {\n          // Set corresponding property to false\n          elem[ propName ] = false;\n        }\n\n        elem.removeAttribute( name );\n      }\n    }\n  },\n\n  attrHooks: {\n    type: {\n      set: function( elem, value ) {\n        if ( !jQuery.support.radioValue && value === \"radio\" && jQuery.nodeName(elem, \"input\") ) {\n          // Setting the type on a radio button after the value resets the value in IE6-9\n          // Reset value to default in case type is set after value during creation\n          var val = elem.value;\n          elem.setAttribute( \"type\", value );\n          if ( val ) {\n            elem.value = val;\n          }\n          return value;\n        }\n      }\n    }\n  },\n\n  propFix: {\n    \"for\": \"htmlFor\",\n    \"class\": \"className\"\n  },\n\n  prop: function( elem, name, value ) {\n    var ret, hooks, notxml,\n      nType = elem.nodeType;\n\n    // don't get/set properties on text, comment and attribute nodes\n    if ( !elem || nType === 3 || nType === 8 || nType === 2 ) {\n      return;\n    }\n\n    notxml = nType !== 1 || !jQuery.isXMLDoc( elem );\n\n    if ( notxml ) {\n      // Fix name and attach hooks\n      name = jQuery.propFix[ name ] || name;\n      hooks = jQuery.propHooks[ name ];\n    }\n\n    if ( value !== undefined ) {\n      return hooks && \"set\" in hooks && (ret = hooks.set( elem, value, name )) !== undefined ?\n        ret :\n        ( elem[ name ] = value );\n\n    } else {\n      return hooks && \"get\" in hooks && (ret = hooks.get( elem, name )) !== null ?\n        ret :\n        elem[ name ];\n    }\n  },\n\n  propHooks: {\n    tabIndex: {\n      get: function( elem ) {\n        return elem.hasAttribute( \"tabindex\" ) || rfocusable.test( elem.nodeName ) || elem.href ?\n          elem.tabIndex :\n          -1;\n      }\n    }\n  }\n});\n\n// Hooks for boolean attributes\nboolHook = {\n  set: function( elem, value, name ) {\n    if ( value === false ) {\n      // Remove boolean attributes when set to false\n      jQuery.removeAttr( elem, name );\n    } else {\n      elem.setAttribute( name, name );\n    }\n    return name;\n  }\n};\njQuery.each( jQuery.expr.match.bool.source.match( /\\w+/g ), function( i, name ) {\n  var getter = jQuery.expr.attrHandle[ name ] || jQuery.find.attr;\n\n  jQuery.expr.attrHandle[ name ] = function( elem, name, isXML ) {\n    var fn = jQuery.expr.attrHandle[ name ],\n      ret = isXML ?\n        undefined :\n        /* jshint eqeqeq: false */\n        // Temporarily disable this handler to check existence\n        (jQuery.expr.attrHandle[ name ] = undefined) !=\n          getter( elem, name, isXML ) ?\n\n          name.toLowerCase() :\n          null;\n\n    // Restore handler\n    jQuery.expr.attrHandle[ name ] = fn;\n\n    return ret;\n  };\n});\n\n// Support: IE9+\n// Selectedness for an option in an optgroup can be inaccurate\nif ( !jQuery.support.optSelected ) {\n  jQuery.propHooks.selected = {\n    get: function( elem ) {\n      var parent = elem.parentNode;\n      if ( parent && parent.parentNode ) {\n        parent.parentNode.selectedIndex;\n      }\n      return null;\n    }\n  };\n}\n\njQuery.each([\n  \"tabIndex\",\n  \"readOnly\",\n  \"maxLength\",\n  \"cellSpacing\",\n  \"cellPadding\",\n  \"rowSpan\",\n  \"colSpan\",\n  \"useMap\",\n  \"frameBorder\",\n  \"contentEditable\"\n], function() {\n  jQuery.propFix[ this.toLowerCase() ] = this;\n});\n\n// Radios and checkboxes getter/setter\njQuery.each([ \"radio\", \"checkbox\" ], function() {\n  jQuery.valHooks[ this ] = {\n    set: function( elem, value ) {\n      if ( jQuery.isArray( value ) ) {\n        return ( elem.checked = jQuery.inArray( jQuery(elem).val(), value ) >= 0 );\n      }\n    }\n  };\n  if ( !jQuery.support.checkOn ) {\n    jQuery.valHooks[ this ].get = function( elem ) {\n      // Support: Webkit\n      // \"\" is returned instead of \"on\" if a value isn't specified\n      return elem.getAttribute(\"value\") === null ? \"on\" : elem.value;\n    };\n  }\n});\nvar rkeyEvent = /^key/,\n  rmouseEvent = /^(?:mouse|contextmenu)|click/,\n  rfocusMorph = /^(?:focusinfocus|focusoutblur)$/,\n  rtypenamespace = /^([^.]*)(?:\\.(.+)|)$/;\n\nfunction returnTrue() {\n  return true;\n}\n\nfunction returnFalse() {\n  return false;\n}\n\nfunction safeActiveElement() {\n  try {\n    return document.activeElement;\n  } catch ( err ) { }\n}\n\n/*\n * Helper functions for managing events -- not part of the public interface.\n * Props to Dean Edwards' addEvent library for many of the ideas.\n */\njQuery.event = {\n\n  global: {},\n\n  add: function( elem, types, handler, data, selector ) {\n\n    var handleObjIn, eventHandle, tmp,\n      events, t, handleObj,\n      special, handlers, type, namespaces, origType,\n      elemData = data_priv.get( elem );\n\n    // Don't attach events to noData or text/comment nodes (but allow plain objects)\n    if ( !elemData ) {\n      return;\n    }\n\n    // Caller can pass in an object of custom data in lieu of the handler\n    if ( handler.handler ) {\n      handleObjIn = handler;\n      handler = handleObjIn.handler;\n      selector = handleObjIn.selector;\n    }\n\n    // Make sure that the handler has a unique ID, used to find/remove it later\n    if ( !handler.guid ) {\n      handler.guid = jQuery.guid++;\n    }\n\n    // Init the element's event structure and main handler, if this is the first\n    if ( !(events = elemData.events) ) {\n      events = elemData.events = {};\n    }\n    if ( !(eventHandle = elemData.handle) ) {\n      eventHandle = elemData.handle = function( e ) {\n        // Discard the second event of a jQuery.event.trigger() and\n        // when an event is called after a page has unloaded\n        return typeof jQuery !== core_strundefined && (!e || jQuery.event.triggered !== e.type) ?\n          jQuery.event.dispatch.apply( eventHandle.elem, arguments ) :\n          undefined;\n      };\n      // Add elem as a property of the handle fn to prevent a memory leak with IE non-native events\n      eventHandle.elem = elem;\n    }\n\n    // Handle multiple events separated by a space\n    types = ( types || \"\" ).match( core_rnotwhite ) || [\"\"];\n    t = types.length;\n    while ( t-- ) {\n      tmp = rtypenamespace.exec( types[t] ) || [];\n      type = origType = tmp[1];\n      namespaces = ( tmp[2] || \"\" ).split( \".\" ).sort();\n\n      // There *must* be a type, no attaching namespace-only handlers\n      if ( !type ) {\n        continue;\n      }\n\n      // If event changes its type, use the special event handlers for the changed type\n      special = jQuery.event.special[ type ] || {};\n\n      // If selector defined, determine special event api type, otherwise given type\n      type = ( selector ? special.delegateType : special.bindType ) || type;\n\n      // Update special based on newly reset type\n      special = jQuery.event.special[ type ] || {};\n\n      // handleObj is passed to all event handlers\n      handleObj = jQuery.extend({\n        type: type,\n        origType: origType,\n        data: data,\n        handler: handler,\n        guid: handler.guid,\n        selector: selector,\n        needsContext: selector && jQuery.expr.match.needsContext.test( selector ),\n        namespace: namespaces.join(\".\")\n      }, handleObjIn );\n\n      // Init the event handler queue if we're the first\n      if ( !(handlers = events[ type ]) ) {\n        handlers = events[ type ] = [];\n        handlers.delegateCount = 0;\n\n        // Only use addEventListener if the special events handler returns false\n        if ( !special.setup || special.setup.call( elem, data, namespaces, eventHandle ) === false ) {\n          if ( elem.addEventListener ) {\n            elem.addEventListener( type, eventHandle, false );\n          }\n        }\n      }\n\n      if ( special.add ) {\n        special.add.call( elem, handleObj );\n\n        if ( !handleObj.handler.guid ) {\n          handleObj.handler.guid = handler.guid;\n        }\n      }\n\n      // Add to the element's handler list, delegates in front\n      if ( selector ) {\n        handlers.splice( handlers.delegateCount++, 0, handleObj );\n      } else {\n        handlers.push( handleObj );\n      }\n\n      // Keep track of which events have ever been used, for event optimization\n      jQuery.event.global[ type ] = true;\n    }\n\n    // Nullify elem to prevent memory leaks in IE\n    elem = null;\n  },\n\n  // Detach an event or set of events from an element\n  remove: function( elem, types, handler, selector, mappedTypes ) {\n\n    var j, origCount, tmp,\n      events, t, handleObj,\n      special, handlers, type, namespaces, origType,\n      elemData = data_priv.hasData( elem ) && data_priv.get( elem );\n\n    if ( !elemData || !(events = elemData.events) ) {\n      return;\n    }\n\n    // Once for each type.namespace in types; type may be omitted\n    types = ( types || \"\" ).match( core_rnotwhite ) || [\"\"];\n    t = types.length;\n    while ( t-- ) {\n      tmp = rtypenamespace.exec( types[t] ) || [];\n      type = origType = tmp[1];\n      namespaces = ( tmp[2] || \"\" ).split( \".\" ).sort();\n\n      // Unbind all events (on this namespace, if provided) for the element\n      if ( !type ) {\n        for ( type in events ) {\n          jQuery.event.remove( elem, type + types[ t ], handler, selector, true );\n        }\n        continue;\n      }\n\n      special = jQuery.event.special[ type ] || {};\n      type = ( selector ? special.delegateType : special.bindType ) || type;\n      handlers = events[ type ] || [];\n      tmp = tmp[2] && new RegExp( \"(^|\\\\.)\" + namespaces.join(\"\\\\.(?:.*\\\\.|)\") + \"(\\\\.|$)\" );\n\n      // Remove matching events\n      origCount = j = handlers.length;\n      while ( j-- ) {\n        handleObj = handlers[ j ];\n\n        if ( ( mappedTypes || origType === handleObj.origType ) &&\n          ( !handler || handler.guid === handleObj.guid ) &&\n          ( !tmp || tmp.test( handleObj.namespace ) ) &&\n          ( !selector || selector === handleObj.selector || selector === \"**\" && handleObj.selector ) ) {\n          handlers.splice( j, 1 );\n\n          if ( handleObj.selector ) {\n            handlers.delegateCount--;\n          }\n          if ( special.remove ) {\n            special.remove.call( elem, handleObj );\n          }\n        }\n      }\n\n      // Remove generic event handler if we removed something and no more handlers exist\n      // (avoids potential for endless recursion during removal of special event handlers)\n      if ( origCount && !handlers.length ) {\n        if ( !special.teardown || special.teardown.call( elem, namespaces, elemData.handle ) === false ) {\n          jQuery.removeEvent( elem, type, elemData.handle );\n        }\n\n        delete events[ type ];\n      }\n    }\n\n    // Remove the expando if it's no longer used\n    if ( jQuery.isEmptyObject( events ) ) {\n      delete elemData.handle;\n      data_priv.remove( elem, \"events\" );\n    }\n  },\n\n  trigger: function( event, data, elem, onlyHandlers ) {\n\n    var i, cur, tmp, bubbleType, ontype, handle, special,\n      eventPath = [ elem || document ],\n      type = core_hasOwn.call( event, \"type\" ) ? event.type : event,\n      namespaces = core_hasOwn.call( event, \"namespace\" ) ? event.namespace.split(\".\") : [];\n\n    cur = tmp = elem = elem || document;\n\n    // Don't do events on text and comment nodes\n    if ( elem.nodeType === 3 || elem.nodeType === 8 ) {\n      return;\n    }\n\n    // focus/blur morphs to focusin/out; ensure we're not firing them right now\n    if ( rfocusMorph.test( type + jQuery.event.triggered ) ) {\n      return;\n    }\n\n    if ( type.indexOf(\".\") >= 0 ) {\n      // Namespaced trigger; create a regexp to match event type in handle()\n      namespaces = type.split(\".\");\n      type = namespaces.shift();\n      namespaces.sort();\n    }\n    ontype = type.indexOf(\":\") < 0 && \"on\" + type;\n\n    // Caller can pass in a jQuery.Event object, Object, or just an event type string\n    event = event[ jQuery.expando ] ?\n      event :\n      new jQuery.Event( type, typeof event === \"object\" && event );\n\n    // Trigger bitmask: & 1 for native handlers; & 2 for jQuery (always true)\n    event.isTrigger = onlyHandlers ? 2 : 3;\n    event.namespace = namespaces.join(\".\");\n    event.namespace_re = event.namespace ?\n      new RegExp( \"(^|\\\\.)\" + namespaces.join(\"\\\\.(?:.*\\\\.|)\") + \"(\\\\.|$)\" ) :\n      null;\n\n    // Clean up the event in case it is being reused\n    event.result = undefined;\n    if ( !event.target ) {\n      event.target = elem;\n    }\n\n    // Clone any incoming data and prepend the event, creating the handler arg list\n    data = data == null ?\n      [ event ] :\n      jQuery.makeArray( data, [ event ] );\n\n    // Allow special events to draw outside the lines\n    special = jQuery.event.special[ type ] || {};\n    if ( !onlyHandlers && special.trigger && special.trigger.apply( elem, data ) === false ) {\n      return;\n    }\n\n    // Determine event propagation path in advance, per W3C events spec (#9951)\n    // Bubble up to document, then to window; watch for a global ownerDocument var (#9724)\n    if ( !onlyHandlers && !special.noBubble && !jQuery.isWindow( elem ) ) {\n\n      bubbleType = special.delegateType || type;\n      if ( !rfocusMorph.test( bubbleType + type ) ) {\n        cur = cur.parentNode;\n      }\n      for ( ; cur; cur = cur.parentNode ) {\n        eventPath.push( cur );\n        tmp = cur;\n      }\n\n      // Only add window if we got to document (e.g., not plain obj or detached DOM)\n      if ( tmp === (elem.ownerDocument || document) ) {\n        eventPath.push( tmp.defaultView || tmp.parentWindow || window );\n      }\n    }\n\n    // Fire handlers on the event path\n    i = 0;\n    while ( (cur = eventPath[i++]) && !event.isPropagationStopped() ) {\n\n      event.type = i > 1 ?\n        bubbleType :\n        special.bindType || type;\n\n      // jQuery handler\n      handle = ( data_priv.get( cur, \"events\" ) || {} )[ event.type ] && data_priv.get( cur, \"handle\" );\n      if ( handle ) {\n        handle.apply( cur, data );\n      }\n\n      // Native handler\n      handle = ontype && cur[ ontype ];\n      if ( handle && jQuery.acceptData( cur ) && handle.apply && handle.apply( cur, data ) === false ) {\n        event.preventDefault();\n      }\n    }\n    event.type = type;\n\n    // If nobody prevented the default action, do it now\n    if ( !onlyHandlers && !event.isDefaultPrevented() ) {\n\n      if ( (!special._default || special._default.apply( eventPath.pop(), data ) === false) &&\n        jQuery.acceptData( elem ) ) {\n\n        // Call a native DOM method on the target with the same name name as the event.\n        // Don't do default actions on window, that's where global variables be (#6170)\n        if ( ontype && jQuery.isFunction( elem[ type ] ) && !jQuery.isWindow( elem ) ) {\n\n          // Don't re-trigger an onFOO event when we call its FOO() method\n          tmp = elem[ ontype ];\n\n          if ( tmp ) {\n            elem[ ontype ] = null;\n          }\n\n          // Prevent re-triggering of the same event, since we already bubbled it above\n          jQuery.event.triggered = type;\n          elem[ type ]();\n          jQuery.event.triggered = undefined;\n\n          if ( tmp ) {\n            elem[ ontype ] = tmp;\n          }\n        }\n      }\n    }\n\n    return event.result;\n  },\n\n  dispatch: function( event ) {\n\n    // Make a writable jQuery.Event from the native event object\n    event = jQuery.event.fix( event );\n\n    var i, j, ret, matched, handleObj,\n      handlerQueue = [],\n      args = core_slice.call( arguments ),\n      handlers = ( data_priv.get( this, \"events\" ) || {} )[ event.type ] || [],\n      special = jQuery.event.special[ event.type ] || {};\n\n    // Use the fix-ed jQuery.Event rather than the (read-only) native event\n    args[0] = event;\n    event.delegateTarget = this;\n\n    // Call the preDispatch hook for the mapped type, and let it bail if desired\n    if ( special.preDispatch && special.preDispatch.call( this, event ) === false ) {\n      return;\n    }\n\n    // Determine handlers\n    handlerQueue = jQuery.event.handlers.call( this, event, handlers );\n\n    // Run delegates first; they may want to stop propagation beneath us\n    i = 0;\n    while ( (matched = handlerQueue[ i++ ]) && !event.isPropagationStopped() ) {\n      event.currentTarget = matched.elem;\n\n      j = 0;\n      while ( (handleObj = matched.handlers[ j++ ]) && !event.isImmediatePropagationStopped() ) {\n\n        // Triggered event must either 1) have no namespace, or\n        // 2) have namespace(s) a subset or equal to those in the bound event (both can have no namespace).\n        if ( !event.namespace_re || event.namespace_re.test( handleObj.namespace ) ) {\n\n          event.handleObj = handleObj;\n          event.data = handleObj.data;\n\n          ret = ( (jQuery.event.special[ handleObj.origType ] || {}).handle || handleObj.handler )\n              .apply( matched.elem, args );\n\n          if ( ret !== undefined ) {\n            if ( (event.result = ret) === false ) {\n              event.preventDefault();\n              event.stopPropagation();\n            }\n          }\n        }\n      }\n    }\n\n    // Call the postDispatch hook for the mapped type\n    if ( special.postDispatch ) {\n      special.postDispatch.call( this, event );\n    }\n\n    return event.result;\n  },\n\n  handlers: function( event, handlers ) {\n    var i, matches, sel, handleObj,\n      handlerQueue = [],\n      delegateCount = handlers.delegateCount,\n      cur = event.target;\n\n    // Find delegate handlers\n    // Black-hole SVG <use> instance trees (#13180)\n    // Avoid non-left-click bubbling in Firefox (#3861)\n    if ( delegateCount && cur.nodeType && (!event.button || event.type !== \"click\") ) {\n\n      for ( ; cur !== this; cur = cur.parentNode || this ) {\n\n        // Don't process clicks on disabled elements (#6911, #8165, #11382, #11764)\n        if ( cur.disabled !== true || event.type !== \"click\" ) {\n          matches = [];\n          for ( i = 0; i < delegateCount; i++ ) {\n            handleObj = handlers[ i ];\n\n            // Don't conflict with Object.prototype properties (#13203)\n            sel = handleObj.selector + \" \";\n\n            if ( matches[ sel ] === undefined ) {\n              matches[ sel ] = handleObj.needsContext ?\n                jQuery( sel, this ).index( cur ) >= 0 :\n                jQuery.find( sel, this, null, [ cur ] ).length;\n            }\n            if ( matches[ sel ] ) {\n              matches.push( handleObj );\n            }\n          }\n          if ( matches.length ) {\n            handlerQueue.push({ elem: cur, handlers: matches });\n          }\n        }\n      }\n    }\n\n    // Add the remaining (directly-bound) handlers\n    if ( delegateCount < handlers.length ) {\n      handlerQueue.push({ elem: this, handlers: handlers.slice( delegateCount ) });\n    }\n\n    return handlerQueue;\n  },\n\n  // Includes some event props shared by KeyEvent and MouseEvent\n  props: \"altKey bubbles cancelable ctrlKey currentTarget eventPhase metaKey relatedTarget shiftKey target timeStamp view which\".split(\" \"),\n\n  fixHooks: {},\n\n  keyHooks: {\n    props: \"char charCode key keyCode\".split(\" \"),\n    filter: function( event, original ) {\n\n      // Add which for key events\n      if ( event.which == null ) {\n        event.which = original.charCode != null ? original.charCode : original.keyCode;\n      }\n\n      return event;\n    }\n  },\n\n  mouseHooks: {\n    props: \"button buttons clientX clientY offsetX offsetY pageX pageY screenX screenY toElement\".split(\" \"),\n    filter: function( event, original ) {\n      var eventDoc, doc, body,\n        button = original.button;\n\n      // Calculate pageX/Y if missing and clientX/Y available\n      if ( event.pageX == null && original.clientX != null ) {\n        eventDoc = event.target.ownerDocument || document;\n        doc = eventDoc.documentElement;\n        body = eventDoc.body;\n\n        event.pageX = original.clientX + ( doc && doc.scrollLeft || body && body.scrollLeft || 0 ) - ( doc && doc.clientLeft || body && body.clientLeft || 0 );\n        event.pageY = original.clientY + ( doc && doc.scrollTop  || body && body.scrollTop  || 0 ) - ( doc && doc.clientTop  || body && body.clientTop  || 0 );\n      }\n\n      // Add which for click: 1 === left; 2 === middle; 3 === right\n      // Note: button is not normalized, so don't use it\n      if ( !event.which && button !== undefined ) {\n        event.which = ( button & 1 ? 1 : ( button & 2 ? 3 : ( button & 4 ? 2 : 0 ) ) );\n      }\n\n      return event;\n    }\n  },\n\n  fix: function( event ) {\n    if ( event[ jQuery.expando ] ) {\n      return event;\n    }\n\n    // Create a writable copy of the event object and normalize some properties\n    var i, prop, copy,\n      type = event.type,\n      originalEvent = event,\n      fixHook = this.fixHooks[ type ];\n\n    if ( !fixHook ) {\n      this.fixHooks[ type ] = fixHook =\n        rmouseEvent.test( type ) ? this.mouseHooks :\n        rkeyEvent.test( type ) ? this.keyHooks :\n        {};\n    }\n    copy = fixHook.props ? this.props.concat( fixHook.props ) : this.props;\n\n    event = new jQuery.Event( originalEvent );\n\n    i = copy.length;\n    while ( i-- ) {\n      prop = copy[ i ];\n      event[ prop ] = originalEvent[ prop ];\n    }\n\n    // Support: Cordova 2.5 (WebKit) (#13255)\n    // All events should have a target; Cordova deviceready doesn't\n    if ( !event.target ) {\n      event.target = document;\n    }\n\n    // Support: Safari 6.0+, Chrome < 28\n    // Target should not be a text node (#504, #13143)\n    if ( event.target.nodeType === 3 ) {\n      event.target = event.target.parentNode;\n    }\n\n    return fixHook.filter? fixHook.filter( event, originalEvent ) : event;\n  },\n\n  special: {\n    load: {\n      // Prevent triggered image.load events from bubbling to window.load\n      noBubble: true\n    },\n    focus: {\n      // Fire native event if possible so blur/focus sequence is correct\n      trigger: function() {\n        if ( this !== safeActiveElement() && this.focus ) {\n          this.focus();\n          return false;\n        }\n      },\n      delegateType: \"focusin\"\n    },\n    blur: {\n      trigger: function() {\n        if ( this === safeActiveElement() && this.blur ) {\n          this.blur();\n          return false;\n        }\n      },\n      delegateType: \"focusout\"\n    },\n    click: {\n      // For checkbox, fire native event so checked state will be right\n      trigger: function() {\n        if ( this.type === \"checkbox\" && this.click && jQuery.nodeName( this, \"input\" ) ) {\n          this.click();\n          return false;\n        }\n      },\n\n      // For cross-browser consistency, don't fire native .click() on links\n      _default: function( event ) {\n        return jQuery.nodeName( event.target, \"a\" );\n      }\n    },\n\n    beforeunload: {\n      postDispatch: function( event ) {\n\n        // Support: Firefox 20+\n        // Firefox doesn't alert if the returnValue field is not set.\n        if ( event.result !== undefined ) {\n          event.originalEvent.returnValue = event.result;\n        }\n      }\n    }\n  },\n\n  simulate: function( type, elem, event, bubble ) {\n    // Piggyback on a donor event to simulate a different one.\n    // Fake originalEvent to avoid donor's stopPropagation, but if the\n    // simulated event prevents default then we do the same on the donor.\n    var e = jQuery.extend(\n      new jQuery.Event(),\n      event,\n      {\n        type: type,\n        isSimulated: true,\n        originalEvent: {}\n      }\n    );\n    if ( bubble ) {\n      jQuery.event.trigger( e, null, elem );\n    } else {\n      jQuery.event.dispatch.call( elem, e );\n    }\n    if ( e.isDefaultPrevented() ) {\n      event.preventDefault();\n    }\n  }\n};\n\njQuery.removeEvent = function( elem, type, handle ) {\n  if ( elem.removeEventListener ) {\n    elem.removeEventListener( type, handle, false );\n  }\n};\n\njQuery.Event = function( src, props ) {\n  // Allow instantiation without the 'new' keyword\n  if ( !(this instanceof jQuery.Event) ) {\n    return new jQuery.Event( src, props );\n  }\n\n  // Event object\n  if ( src && src.type ) {\n    this.originalEvent = src;\n    this.type = src.type;\n\n    // Events bubbling up the document may have been marked as prevented\n    // by a handler lower down the tree; reflect the correct value.\n    this.isDefaultPrevented = ( src.defaultPrevented ||\n      src.getPreventDefault && src.getPreventDefault() ) ? returnTrue : returnFalse;\n\n  // Event type\n  } else {\n    this.type = src;\n  }\n\n  // Put explicitly provided properties onto the event object\n  if ( props ) {\n    jQuery.extend( this, props );\n  }\n\n  // Create a timestamp if incoming event doesn't have one\n  this.timeStamp = src && src.timeStamp || jQuery.now();\n\n  // Mark it as fixed\n  this[ jQuery.expando ] = true;\n};\n\n// jQuery.Event is based on DOM3 Events as specified by the ECMAScript Language Binding\n// http://www.w3.org/TR/2003/WD-DOM-Level-3-Events-20030331/ecma-script-binding.html\njQuery.Event.prototype = {\n  isDefaultPrevented: returnFalse,\n  isPropagationStopped: returnFalse,\n  isImmediatePropagationStopped: returnFalse,\n\n  preventDefault: function() {\n    var e = this.originalEvent;\n\n    this.isDefaultPrevented = returnTrue;\n\n    if ( e && e.preventDefault ) {\n      e.preventDefault();\n    }\n  },\n  stopPropagation: function() {\n    var e = this.originalEvent;\n\n    this.isPropagationStopped = returnTrue;\n\n    if ( e && e.stopPropagation ) {\n      e.stopPropagation();\n    }\n  },\n  stopImmediatePropagation: function() {\n    this.isImmediatePropagationStopped = returnTrue;\n    this.stopPropagation();\n  }\n};\n\n// Create mouseenter/leave events using mouseover/out and event-time checks\n// Support: Chrome 15+\njQuery.each({\n  mouseenter: \"mouseover\",\n  mouseleave: \"mouseout\"\n}, function( orig, fix ) {\n  jQuery.event.special[ orig ] = {\n    delegateType: fix,\n    bindType: fix,\n\n    handle: function( event ) {\n      var ret,\n        target = this,\n        related = event.relatedTarget,\n        handleObj = event.handleObj;\n\n      // For mousenter/leave call the handler if related is outside the target.\n      // NB: No relatedTarget if the mouse left/entered the browser window\n      if ( !related || (related !== target && !jQuery.contains( target, related )) ) {\n        event.type = handleObj.origType;\n        ret = handleObj.handler.apply( this, arguments );\n        event.type = fix;\n      }\n      return ret;\n    }\n  };\n});\n\n// Create \"bubbling\" focus and blur events\n// Support: Firefox, Chrome, Safari\nif ( !jQuery.support.focusinBubbles ) {\n  jQuery.each({ focus: \"focusin\", blur: \"focusout\" }, function( orig, fix ) {\n\n    // Attach a single capturing handler while someone wants focusin/focusout\n    var attaches = 0,\n      handler = function( event ) {\n        jQuery.event.simulate( fix, event.target, jQuery.event.fix( event ), true );\n      };\n\n    jQuery.event.special[ fix ] = {\n      setup: function() {\n        if ( attaches++ === 0 ) {\n          document.addEventListener( orig, handler, true );\n        }\n      },\n      teardown: function() {\n        if ( --attaches === 0 ) {\n          document.removeEventListener( orig, handler, true );\n        }\n      }\n    };\n  });\n}\n\njQuery.fn.extend({\n\n  on: function( types, selector, data, fn, /*INTERNAL*/ one ) {\n    var origFn, type;\n\n    // Types can be a map of types/handlers\n    if ( typeof types === \"object\" ) {\n      // ( types-Object, selector, data )\n      if ( typeof selector !== \"string\" ) {\n        // ( types-Object, data )\n        data = data || selector;\n        selector = undefined;\n      }\n      for ( type in types ) {\n        this.on( type, selector, data, types[ type ], one );\n      }\n      return this;\n    }\n\n    if ( data == null && fn == null ) {\n      // ( types, fn )\n      fn = selector;\n      data = selector = undefined;\n    } else if ( fn == null ) {\n      if ( typeof selector === \"string\" ) {\n        // ( types, selector, fn )\n        fn = data;\n        data = undefined;\n      } else {\n        // ( types, data, fn )\n        fn = data;\n        data = selector;\n        selector = undefined;\n      }\n    }\n    if ( fn === false ) {\n      fn = returnFalse;\n    } else if ( !fn ) {\n      return this;\n    }\n\n    if ( one === 1 ) {\n      origFn = fn;\n      fn = function( event ) {\n        // Can use an empty set, since event contains the info\n        jQuery().off( event );\n        return origFn.apply( this, arguments );\n      };\n      // Use same guid so caller can remove using origFn\n      fn.guid = origFn.guid || ( origFn.guid = jQuery.guid++ );\n    }\n    return this.each( function() {\n      jQuery.event.add( this, types, fn, data, selector );\n    });\n  },\n  one: function( types, selector, data, fn ) {\n    return this.on( types, selector, data, fn, 1 );\n  },\n  off: function( types, selector, fn ) {\n    var handleObj, type;\n    if ( types && types.preventDefault && types.handleObj ) {\n      // ( event )  dispatched jQuery.Event\n      handleObj = types.handleObj;\n      jQuery( types.delegateTarget ).off(\n        handleObj.namespace ? handleObj.origType + \".\" + handleObj.namespace : handleObj.origType,\n        handleObj.selector,\n        handleObj.handler\n      );\n      return this;\n    }\n    if ( typeof types === \"object\" ) {\n      // ( types-object [, selector] )\n      for ( type in types ) {\n        this.off( type, selector, types[ type ] );\n      }\n      return this;\n    }\n    if ( selector === false || typeof selector === \"function\" ) {\n      // ( types [, fn] )\n      fn = selector;\n      selector = undefined;\n    }\n    if ( fn === false ) {\n      fn = returnFalse;\n    }\n    return this.each(function() {\n      jQuery.event.remove( this, types, fn, selector );\n    });\n  },\n\n  trigger: function( type, data ) {\n    return this.each(function() {\n      jQuery.event.trigger( type, data, this );\n    });\n  },\n  triggerHandler: function( type, data ) {\n    var elem = this[0];\n    if ( elem ) {\n      return jQuery.event.trigger( type, data, elem, true );\n    }\n  }\n});\nvar isSimple = /^.[^:#\\[\\.,]*$/,\n  rparentsprev = /^(?:parents|prev(?:Until|All))/,\n  rneedsContext = jQuery.expr.match.needsContext,\n  // methods guaranteed to produce a unique set when starting from a unique set\n  guaranteedUnique = {\n    children: true,\n    contents: true,\n    next: true,\n    prev: true\n  };\n\njQuery.fn.extend({\n  find: function( selector ) {\n    var i,\n      ret = [],\n      self = this,\n      len = self.length;\n\n    if ( typeof selector !== \"string\" ) {\n      return this.pushStack( jQuery( selector ).filter(function() {\n        for ( i = 0; i < len; i++ ) {\n          if ( jQuery.contains( self[ i ], this ) ) {\n            return true;\n          }\n        }\n      }) );\n    }\n\n    for ( i = 0; i < len; i++ ) {\n      jQuery.find( selector, self[ i ], ret );\n    }\n\n    // Needed because $( selector, context ) becomes $( context ).find( selector )\n    ret = this.pushStack( len > 1 ? jQuery.unique( ret ) : ret );\n    ret.selector = this.selector ? this.selector + \" \" + selector : selector;\n    return ret;\n  },\n\n  has: function( target ) {\n    var targets = jQuery( target, this ),\n      l = targets.length;\n\n    return this.filter(function() {\n      var i = 0;\n      for ( ; i < l; i++ ) {\n        if ( jQuery.contains( this, targets[i] ) ) {\n          return true;\n        }\n      }\n    });\n  },\n\n  not: function( selector ) {\n    return this.pushStack( winnow(this, selector || [], true) );\n  },\n\n  filter: function( selector ) {\n    return this.pushStack( winnow(this, selector || [], false) );\n  },\n\n  is: function( selector ) {\n    return !!winnow(\n      this,\n\n      // If this is a positional/relative selector, check membership in the returned set\n      // so $(\"p:first\").is(\"p:last\") won't return true for a doc with two \"p\".\n      typeof selector === \"string\" && rneedsContext.test( selector ) ?\n        jQuery( selector ) :\n        selector || [],\n      false\n    ).length;\n  },\n\n  closest: function( selectors, context ) {\n    var cur,\n      i = 0,\n      l = this.length,\n      matched = [],\n      pos = ( rneedsContext.test( selectors ) || typeof selectors !== \"string\" ) ?\n        jQuery( selectors, context || this.context ) :\n        0;\n\n    for ( ; i < l; i++ ) {\n      for ( cur = this[i]; cur && cur !== context; cur = cur.parentNode ) {\n        // Always skip document fragments\n        if ( cur.nodeType < 11 && (pos ?\n          pos.index(cur) > -1 :\n\n          // Don't pass non-elements to Sizzle\n          cur.nodeType === 1 &&\n            jQuery.find.matchesSelector(cur, selectors)) ) {\n\n          cur = matched.push( cur );\n          break;\n        }\n      }\n    }\n\n    return this.pushStack( matched.length > 1 ? jQuery.unique( matched ) : matched );\n  },\n\n  // Determine the position of an element within\n  // the matched set of elements\n  index: function( elem ) {\n\n    // No argument, return index in parent\n    if ( !elem ) {\n      return ( this[ 0 ] && this[ 0 ].parentNode ) ? this.first().prevAll().length : -1;\n    }\n\n    // index in selector\n    if ( typeof elem === \"string\" ) {\n      return core_indexOf.call( jQuery( elem ), this[ 0 ] );\n    }\n\n    // Locate the position of the desired element\n    return core_indexOf.call( this,\n\n      // If it receives a jQuery object, the first element is used\n      elem.jquery ? elem[ 0 ] : elem\n    );\n  },\n\n  add: function( selector, context ) {\n    var set = typeof selector === \"string\" ?\n        jQuery( selector, context ) :\n        jQuery.makeArray( selector && selector.nodeType ? [ selector ] : selector ),\n      all = jQuery.merge( this.get(), set );\n\n    return this.pushStack( jQuery.unique(all) );\n  },\n\n  addBack: function( selector ) {\n    return this.add( selector == null ?\n      this.prevObject : this.prevObject.filter(selector)\n    );\n  }\n});\n\nfunction sibling( cur, dir ) {\n  while ( (cur = cur[dir]) && cur.nodeType !== 1 ) {}\n\n  return cur;\n}\n\njQuery.each({\n  parent: function( elem ) {\n    var parent = elem.parentNode;\n    return parent && parent.nodeType !== 11 ? parent : null;\n  },\n  parents: function( elem ) {\n    return jQuery.dir( elem, \"parentNode\" );\n  },\n  parentsUntil: function( elem, i, until ) {\n    return jQuery.dir( elem, \"parentNode\", until );\n  },\n  next: function( elem ) {\n    return sibling( elem, \"nextSibling\" );\n  },\n  prev: function( elem ) {\n    return sibling( elem, \"previousSibling\" );\n  },\n  nextAll: function( elem ) {\n    return jQuery.dir( elem, \"nextSibling\" );\n  },\n  prevAll: function( elem ) {\n    return jQuery.dir( elem, \"previousSibling\" );\n  },\n  nextUntil: function( elem, i, until ) {\n    return jQuery.dir( elem, \"nextSibling\", until );\n  },\n  prevUntil: function( elem, i, until ) {\n    return jQuery.dir( elem, \"previousSibling\", until );\n  },\n  siblings: function( elem ) {\n    return jQuery.sibling( ( elem.parentNode || {} ).firstChild, elem );\n  },\n  children: function( elem ) {\n    return jQuery.sibling( elem.firstChild );\n  },\n  contents: function( elem ) {\n    return elem.contentDocument || jQuery.merge( [], elem.childNodes );\n  }\n}, function( name, fn ) {\n  jQuery.fn[ name ] = function( until, selector ) {\n    var matched = jQuery.map( this, fn, until );\n\n    if ( name.slice( -5 ) !== \"Until\" ) {\n      selector = until;\n    }\n\n    if ( selector && typeof selector === \"string\" ) {\n      matched = jQuery.filter( selector, matched );\n    }\n\n    if ( this.length > 1 ) {\n      // Remove duplicates\n      if ( !guaranteedUnique[ name ] ) {\n        jQuery.unique( matched );\n      }\n\n      // Reverse order for parents* and prev-derivatives\n      if ( rparentsprev.test( name ) ) {\n        matched.reverse();\n      }\n    }\n\n    return this.pushStack( matched );\n  };\n});\n\njQuery.extend({\n  filter: function( expr, elems, not ) {\n    var elem = elems[ 0 ];\n\n    if ( not ) {\n      expr = \":not(\" + expr + \")\";\n    }\n\n    return elems.length === 1 && elem.nodeType === 1 ?\n      jQuery.find.matchesSelector( elem, expr ) ? [ elem ] : [] :\n      jQuery.find.matches( expr, jQuery.grep( elems, function( elem ) {\n        return elem.nodeType === 1;\n      }));\n  },\n\n  dir: function( elem, dir, until ) {\n    var matched = [],\n      truncate = until !== undefined;\n\n    while ( (elem = elem[ dir ]) && elem.nodeType !== 9 ) {\n      if ( elem.nodeType === 1 ) {\n        if ( truncate && jQuery( elem ).is( until ) ) {\n          break;\n        }\n        matched.push( elem );\n      }\n    }\n    return matched;\n  },\n\n  sibling: function( n, elem ) {\n    var matched = [];\n\n    for ( ; n; n = n.nextSibling ) {\n      if ( n.nodeType === 1 && n !== elem ) {\n        matched.push( n );\n      }\n    }\n\n    return matched;\n  }\n});\n\n// Implement the identical functionality for filter and not\nfunction winnow( elements, qualifier, not ) {\n  if ( jQuery.isFunction( qualifier ) ) {\n    return jQuery.grep( elements, function( elem, i ) {\n      /* jshint -W018 */\n      return !!qualifier.call( elem, i, elem ) !== not;\n    });\n\n  }\n\n  if ( qualifier.nodeType ) {\n    return jQuery.grep( elements, function( elem ) {\n      return ( elem === qualifier ) !== not;\n    });\n\n  }\n\n  if ( typeof qualifier === \"string\" ) {\n    if ( isSimple.test( qualifier ) ) {\n      return jQuery.filter( qualifier, elements, not );\n    }\n\n    qualifier = jQuery.filter( qualifier, elements );\n  }\n\n  return jQuery.grep( elements, function( elem ) {\n    return ( core_indexOf.call( qualifier, elem ) >= 0 ) !== not;\n  });\n}\nvar rxhtmlTag = /<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\\w:]+)[^>]*)\\/>/gi,\n  rtagName = /<([\\w:]+)/,\n  rhtml = /<|&#?\\w+;/,\n  rnoInnerhtml = /<(?:script|style|link)/i,\n  manipulation_rcheckableType = /^(?:checkbox|radio)$/i,\n  // checked=\"checked\" or checked\n  rchecked = /checked\\s*(?:[^=]|=\\s*.checked.)/i,\n  rscriptType = /^$|\\/(?:java|ecma)script/i,\n  rscriptTypeMasked = /^true\\/(.*)/,\n  rcleanScript = /^\\s*<!(?:\\[CDATA\\[|--)|(?:\\]\\]|--)>\\s*$/g,\n\n  // We have to close these tags to support XHTML (#13200)\n  wrapMap = {\n\n    // Support: IE 9\n    option: [ 1, \"<select multiple='multiple'>\", \"</select>\" ],\n\n    thead: [ 1, \"<table>\", \"</table>\" ],\n    col: [ 2, \"<table><colgroup>\", \"</colgroup></table>\" ],\n    tr: [ 2, \"<table><tbody>\", \"</tbody></table>\" ],\n    td: [ 3, \"<table><tbody><tr>\", \"</tr></tbody></table>\" ],\n\n    _default: [ 0, \"\", \"\" ]\n  };\n\n// Support: IE 9\nwrapMap.optgroup = wrapMap.option;\n\nwrapMap.tbody = wrapMap.tfoot = wrapMap.colgroup = wrapMap.caption = wrapMap.thead;\nwrapMap.th = wrapMap.td;\n\njQuery.fn.extend({\n  text: function( value ) {\n    return jQuery.access( this, function( value ) {\n      return value === undefined ?\n        jQuery.text( this ) :\n        this.empty().append( ( this[ 0 ] && this[ 0 ].ownerDocument || document ).createTextNode( value ) );\n    }, null, value, arguments.length );\n  },\n\n  append: function() {\n    return this.domManip( arguments, function( elem ) {\n      if ( this.nodeType === 1 || this.nodeType === 11 || this.nodeType === 9 ) {\n        var target = manipulationTarget( this, elem );\n        target.appendChild( elem );\n      }\n    });\n  },\n\n  prepend: function() {\n    return this.domManip( arguments, function( elem ) {\n      if ( this.nodeType === 1 || this.nodeType === 11 || this.nodeType === 9 ) {\n        var target = manipulationTarget( this, elem );\n        target.insertBefore( elem, target.firstChild );\n      }\n    });\n  },\n\n  before: function() {\n    return this.domManip( arguments, function( elem ) {\n      if ( this.parentNode ) {\n        this.parentNode.insertBefore( elem, this );\n      }\n    });\n  },\n\n  after: function() {\n    return this.domManip( arguments, function( elem ) {\n      if ( this.parentNode ) {\n        this.parentNode.insertBefore( elem, this.nextSibling );\n      }\n    });\n  },\n\n  // keepData is for internal use only--do not document\n  remove: function( selector, keepData ) {\n    var elem,\n      elems = selector ? jQuery.filter( selector, this ) : this,\n      i = 0;\n\n    for ( ; (elem = elems[i]) != null; i++ ) {\n      if ( !keepData && elem.nodeType === 1 ) {\n        jQuery.cleanData( getAll( elem ) );\n      }\n\n      if ( elem.parentNode ) {\n        if ( keepData && jQuery.contains( elem.ownerDocument, elem ) ) {\n          setGlobalEval( getAll( elem, \"script\" ) );\n        }\n        elem.parentNode.removeChild( elem );\n      }\n    }\n\n    return this;\n  },\n\n  empty: function() {\n    var elem,\n      i = 0;\n\n    for ( ; (elem = this[i]) != null; i++ ) {\n      if ( elem.nodeType === 1 ) {\n\n        // Prevent memory leaks\n        jQuery.cleanData( getAll( elem, false ) );\n\n        // Remove any remaining nodes\n        elem.textContent = \"\";\n      }\n    }\n\n    return this;\n  },\n\n  clone: function( dataAndEvents, deepDataAndEvents ) {\n    dataAndEvents = dataAndEvents == null ? false : dataAndEvents;\n    deepDataAndEvents = deepDataAndEvents == null ? dataAndEvents : deepDataAndEvents;\n\n    return this.map( function () {\n      return jQuery.clone( this, dataAndEvents, deepDataAndEvents );\n    });\n  },\n\n  html: function( value ) {\n    return jQuery.access( this, function( value ) {\n      var elem = this[ 0 ] || {},\n        i = 0,\n        l = this.length;\n\n      if ( value === undefined && elem.nodeType === 1 ) {\n        return elem.innerHTML;\n      }\n\n      // See if we can take a shortcut and just use innerHTML\n      if ( typeof value === \"string\" && !rnoInnerhtml.test( value ) &&\n        !wrapMap[ ( rtagName.exec( value ) || [ \"\", \"\" ] )[ 1 ].toLowerCase() ] ) {\n\n        value = value.replace( rxhtmlTag, \"<$1></$2>\" );\n\n        try {\n          for ( ; i < l; i++ ) {\n            elem = this[ i ] || {};\n\n            // Remove element nodes and prevent memory leaks\n            if ( elem.nodeType === 1 ) {\n              jQuery.cleanData( getAll( elem, false ) );\n              elem.innerHTML = value;\n            }\n          }\n\n          elem = 0;\n\n        // If using innerHTML throws an exception, use the fallback method\n        } catch( e ) {}\n      }\n\n      if ( elem ) {\n        this.empty().append( value );\n      }\n    }, null, value, arguments.length );\n  },\n\n  replaceWith: function() {\n    var\n      // Snapshot the DOM in case .domManip sweeps something relevant into its fragment\n      args = jQuery.map( this, function( elem ) {\n        return [ elem.nextSibling, elem.parentNode ];\n      }),\n      i = 0;\n\n    // Make the changes, replacing each context element with the new content\n    this.domManip( arguments, function( elem ) {\n      var next = args[ i++ ],\n        parent = args[ i++ ];\n\n      if ( parent ) {\n        // Don't use the snapshot next if it has moved (#13810)\n        if ( next && next.parentNode !== parent ) {\n          next = this.nextSibling;\n        }\n        jQuery( this ).remove();\n        parent.insertBefore( elem, next );\n      }\n    // Allow new content to include elements from the context set\n    }, true );\n\n    // Force removal if there was no new content (e.g., from empty arguments)\n    return i ? this : this.remove();\n  },\n\n  detach: function( selector ) {\n    return this.remove( selector, true );\n  },\n\n  domManip: function( args, callback, allowIntersection ) {\n\n    // Flatten any nested arrays\n    args = core_concat.apply( [], args );\n\n    var fragment, first, scripts, hasScripts, node, doc,\n      i = 0,\n      l = this.length,\n      set = this,\n      iNoClone = l - 1,\n      value = args[ 0 ],\n      isFunction = jQuery.isFunction( value );\n\n    // We can't cloneNode fragments that contain checked, in WebKit\n    if ( isFunction || !( l <= 1 || typeof value !== \"string\" || jQuery.support.checkClone || !rchecked.test( value ) ) ) {\n      return this.each(function( index ) {\n        var self = set.eq( index );\n        if ( isFunction ) {\n          args[ 0 ] = value.call( this, index, self.html() );\n        }\n        self.domManip( args, callback, allowIntersection );\n      });\n    }\n\n    if ( l ) {\n      fragment = jQuery.buildFragment( args, this[ 0 ].ownerDocument, false, !allowIntersection && this );\n      first = fragment.firstChild;\n\n      if ( fragment.childNodes.length === 1 ) {\n        fragment = first;\n      }\n\n      if ( first ) {\n        scripts = jQuery.map( getAll( fragment, \"script\" ), disableScript );\n        hasScripts = scripts.length;\n\n        // Use the original fragment for the last item instead of the first because it can end up\n        // being emptied incorrectly in certain situations (#8070).\n        for ( ; i < l; i++ ) {\n          node = fragment;\n\n          if ( i !== iNoClone ) {\n            node = jQuery.clone( node, true, true );\n\n            // Keep references to cloned scripts for later restoration\n            if ( hasScripts ) {\n              // Support: QtWebKit\n              // jQuery.merge because core_push.apply(_, arraylike) throws\n              jQuery.merge( scripts, getAll( node, \"script\" ) );\n            }\n          }\n\n          callback.call( this[ i ], node, i );\n        }\n\n        if ( hasScripts ) {\n          doc = scripts[ scripts.length - 1 ].ownerDocument;\n\n          // Reenable scripts\n          jQuery.map( scripts, restoreScript );\n\n          // Evaluate executable scripts on first document insertion\n          for ( i = 0; i < hasScripts; i++ ) {\n            node = scripts[ i ];\n            if ( rscriptType.test( node.type || \"\" ) &&\n              !data_priv.access( node, \"globalEval\" ) && jQuery.contains( doc, node ) ) {\n\n              if ( node.src ) {\n                // Hope ajax is available...\n                jQuery._evalUrl( node.src );\n              } else {\n                jQuery.globalEval( node.textContent.replace( rcleanScript, \"\" ) );\n              }\n            }\n          }\n        }\n      }\n    }\n\n    return this;\n  }\n});\n\njQuery.each({\n  appendTo: \"append\",\n  prependTo: \"prepend\",\n  insertBefore: \"before\",\n  insertAfter: \"after\",\n  replaceAll: \"replaceWith\"\n}, function( name, original ) {\n  jQuery.fn[ name ] = function( selector ) {\n    var elems,\n      ret = [],\n      insert = jQuery( selector ),\n      last = insert.length - 1,\n      i = 0;\n\n    for ( ; i <= last; i++ ) {\n      elems = i === last ? this : this.clone( true );\n      jQuery( insert[ i ] )[ original ]( elems );\n\n      // Support: QtWebKit\n      // .get() because core_push.apply(_, arraylike) throws\n      core_push.apply( ret, elems.get() );\n    }\n\n    return this.pushStack( ret );\n  };\n});\n\njQuery.extend({\n  clone: function( elem, dataAndEvents, deepDataAndEvents ) {\n    var i, l, srcElements, destElements,\n      clone = elem.cloneNode( true ),\n      inPage = jQuery.contains( elem.ownerDocument, elem );\n\n    // Support: IE >= 9\n    // Fix Cloning issues\n    if ( !jQuery.support.noCloneChecked && ( elem.nodeType === 1 || elem.nodeType === 11 ) && !jQuery.isXMLDoc( elem ) ) {\n\n      // We eschew Sizzle here for performance reasons: http://jsperf.com/getall-vs-sizzle/2\n      destElements = getAll( clone );\n      srcElements = getAll( elem );\n\n      for ( i = 0, l = srcElements.length; i < l; i++ ) {\n        fixInput( srcElements[ i ], destElements[ i ] );\n      }\n    }\n\n    // Copy the events from the original to the clone\n    if ( dataAndEvents ) {\n      if ( deepDataAndEvents ) {\n        srcElements = srcElements || getAll( elem );\n        destElements = destElements || getAll( clone );\n\n        for ( i = 0, l = srcElements.length; i < l; i++ ) {\n          cloneCopyEvent( srcElements[ i ], destElements[ i ] );\n        }\n      } else {\n        cloneCopyEvent( elem, clone );\n      }\n    }\n\n    // Preserve script evaluation history\n    destElements = getAll( clone, \"script\" );\n    if ( destElements.length > 0 ) {\n      setGlobalEval( destElements, !inPage && getAll( elem, \"script\" ) );\n    }\n\n    // Return the cloned set\n    return clone;\n  },\n\n  buildFragment: function( elems, context, scripts, selection ) {\n    var elem, tmp, tag, wrap, contains, j,\n      i = 0,\n      l = elems.length,\n      fragment = context.createDocumentFragment(),\n      nodes = [];\n\n    for ( ; i < l; i++ ) {\n      elem = elems[ i ];\n\n      if ( elem || elem === 0 ) {\n\n        // Add nodes directly\n        if ( jQuery.type( elem ) === \"object\" ) {\n          // Support: QtWebKit\n          // jQuery.merge because core_push.apply(_, arraylike) throws\n          jQuery.merge( nodes, elem.nodeType ? [ elem ] : elem );\n\n        // Convert non-html into a text node\n        } else if ( !rhtml.test( elem ) ) {\n          nodes.push( context.createTextNode( elem ) );\n\n        // Convert html into DOM nodes\n        } else {\n          tmp = tmp || fragment.appendChild( context.createElement(\"div\") );\n\n          // Deserialize a standard representation\n          tag = ( rtagName.exec( elem ) || [\"\", \"\"] )[ 1 ].toLowerCase();\n          wrap = wrapMap[ tag ] || wrapMap._default;\n          tmp.innerHTML = wrap[ 1 ] + elem.replace( rxhtmlTag, \"<$1></$2>\" ) + wrap[ 2 ];\n\n          // Descend through wrappers to the right content\n          j = wrap[ 0 ];\n          while ( j-- ) {\n            tmp = tmp.lastChild;\n          }\n\n          // Support: QtWebKit\n          // jQuery.merge because core_push.apply(_, arraylike) throws\n          jQuery.merge( nodes, tmp.childNodes );\n\n          // Remember the top-level container\n          tmp = fragment.firstChild;\n\n          // Fixes #12346\n          // Support: Webkit, IE\n          tmp.textContent = \"\";\n        }\n      }\n    }\n\n    // Remove wrapper from fragment\n    fragment.textContent = \"\";\n\n    i = 0;\n    while ( (elem = nodes[ i++ ]) ) {\n\n      // #4087 - If origin and destination elements are the same, and this is\n      // that element, do not do anything\n      if ( selection && jQuery.inArray( elem, selection ) !== -1 ) {\n        continue;\n      }\n\n      contains = jQuery.contains( elem.ownerDocument, elem );\n\n      // Append to fragment\n      tmp = getAll( fragment.appendChild( elem ), \"script\" );\n\n      // Preserve script evaluation history\n      if ( contains ) {\n        setGlobalEval( tmp );\n      }\n\n      // Capture executables\n      if ( scripts ) {\n        j = 0;\n        while ( (elem = tmp[ j++ ]) ) {\n          if ( rscriptType.test( elem.type || \"\" ) ) {\n            scripts.push( elem );\n          }\n        }\n      }\n    }\n\n    return fragment;\n  },\n\n  cleanData: function( elems ) {\n    var data, elem, events, type, key, j,\n      special = jQuery.event.special,\n      i = 0;\n\n    for ( ; (elem = elems[ i ]) !== undefined; i++ ) {\n      if ( Data.accepts( elem ) ) {\n        key = elem[ data_priv.expando ];\n\n        if ( key && (data = data_priv.cache[ key ]) ) {\n          events = Object.keys( data.events || {} );\n          if ( events.length ) {\n            for ( j = 0; (type = events[j]) !== undefined; j++ ) {\n              if ( special[ type ] ) {\n                jQuery.event.remove( elem, type );\n\n              // This is a shortcut to avoid jQuery.event.remove's overhead\n              } else {\n                jQuery.removeEvent( elem, type, data.handle );\n              }\n            }\n          }\n          if ( data_priv.cache[ key ] ) {\n            // Discard any remaining `private` data\n            delete data_priv.cache[ key ];\n          }\n        }\n      }\n      // Discard any remaining `user` data\n      delete data_user.cache[ elem[ data_user.expando ] ];\n    }\n  },\n\n  _evalUrl: function( url ) {\n    return jQuery.ajax({\n      url: url,\n      type: \"GET\",\n      dataType: \"script\",\n      async: false,\n      global: false,\n      \"throws\": true\n    });\n  }\n});\n\n// Support: 1.x compatibility\n// Manipulating tables requires a tbody\nfunction manipulationTarget( elem, content ) {\n  return jQuery.nodeName( elem, \"table\" ) &&\n    jQuery.nodeName( content.nodeType === 1 ? content : content.firstChild, \"tr\" ) ?\n\n    elem.getElementsByTagName(\"tbody\")[0] ||\n      elem.appendChild( elem.ownerDocument.createElement(\"tbody\") ) :\n    elem;\n}\n\n// Replace/restore the type attribute of script elements for safe DOM manipulation\nfunction disableScript( elem ) {\n  elem.type = (elem.getAttribute(\"type\") !== null) + \"/\" + elem.type;\n  return elem;\n}\nfunction restoreScript( elem ) {\n  var match = rscriptTypeMasked.exec( elem.type );\n\n  if ( match ) {\n    elem.type = match[ 1 ];\n  } else {\n    elem.removeAttribute(\"type\");\n  }\n\n  return elem;\n}\n\n// Mark scripts as having already been evaluated\nfunction setGlobalEval( elems, refElements ) {\n  var l = elems.length,\n    i = 0;\n\n  for ( ; i < l; i++ ) {\n    data_priv.set(\n      elems[ i ], \"globalEval\", !refElements || data_priv.get( refElements[ i ], \"globalEval\" )\n    );\n  }\n}\n\nfunction cloneCopyEvent( src, dest ) {\n  var i, l, type, pdataOld, pdataCur, udataOld, udataCur, events;\n\n  if ( dest.nodeType !== 1 ) {\n    return;\n  }\n\n  // 1. Copy private data: events, handlers, etc.\n  if ( data_priv.hasData( src ) ) {\n    pdataOld = data_priv.access( src );\n    pdataCur = data_priv.set( dest, pdataOld );\n    events = pdataOld.events;\n\n    if ( events ) {\n      delete pdataCur.handle;\n      pdataCur.events = {};\n\n      for ( type in events ) {\n        for ( i = 0, l = events[ type ].length; i < l; i++ ) {\n          jQuery.event.add( dest, type, events[ type ][ i ] );\n        }\n      }\n    }\n  }\n\n  // 2. Copy user data\n  if ( data_user.hasData( src ) ) {\n    udataOld = data_user.access( src );\n    udataCur = jQuery.extend( {}, udataOld );\n\n    data_user.set( dest, udataCur );\n  }\n}\n\n\nfunction getAll( context, tag ) {\n  var ret = context.getElementsByTagName ? context.getElementsByTagName( tag || \"*\" ) :\n      context.querySelectorAll ? context.querySelectorAll( tag || \"*\" ) :\n      [];\n\n  return tag === undefined || tag && jQuery.nodeName( context, tag ) ?\n    jQuery.merge( [ context ], ret ) :\n    ret;\n}\n\n// Support: IE >= 9\nfunction fixInput( src, dest ) {\n  var nodeName = dest.nodeName.toLowerCase();\n\n  // Fails to persist the checked state of a cloned checkbox or radio button.\n  if ( nodeName === \"input\" && manipulation_rcheckableType.test( src.type ) ) {\n    dest.checked = src.checked;\n\n  // Fails to return the selected option to the default selected state when cloning options\n  } else if ( nodeName === \"input\" || nodeName === \"textarea\" ) {\n    dest.defaultValue = src.defaultValue;\n  }\n}\njQuery.fn.extend({\n  wrapAll: function( html ) {\n    var wrap;\n\n    if ( jQuery.isFunction( html ) ) {\n      return this.each(function( i ) {\n        jQuery( this ).wrapAll( html.call(this, i) );\n      });\n    }\n\n    if ( this[ 0 ] ) {\n\n      // The elements to wrap the target around\n      wrap = jQuery( html, this[ 0 ].ownerDocument ).eq( 0 ).clone( true );\n\n      if ( this[ 0 ].parentNode ) {\n        wrap.insertBefore( this[ 0 ] );\n      }\n\n      wrap.map(function() {\n        var elem = this;\n\n        while ( elem.firstElementChild ) {\n          elem = elem.firstElementChild;\n        }\n\n        return elem;\n      }).append( this );\n    }\n\n    return this;\n  },\n\n  wrapInner: function( html ) {\n    if ( jQuery.isFunction( html ) ) {\n      return this.each(function( i ) {\n        jQuery( this ).wrapInner( html.call(this, i) );\n      });\n    }\n\n    return this.each(function() {\n      var self = jQuery( this ),\n        contents = self.contents();\n\n      if ( contents.length ) {\n        contents.wrapAll( html );\n\n      } else {\n        self.append( html );\n      }\n    });\n  },\n\n  wrap: function( html ) {\n    var isFunction = jQuery.isFunction( html );\n\n    return this.each(function( i ) {\n      jQuery( this ).wrapAll( isFunction ? html.call(this, i) : html );\n    });\n  },\n\n  unwrap: function() {\n    return this.parent().each(function() {\n      if ( !jQuery.nodeName( this, \"body\" ) ) {\n        jQuery( this ).replaceWith( this.childNodes );\n      }\n    }).end();\n  }\n});\nvar curCSS, iframe,\n  // swappable if display is none or starts with table except \"table\", \"table-cell\", or \"table-caption\"\n  // see here for display values: https://developer.mozilla.org/en-US/docs/CSS/display\n  rdisplayswap = /^(none|table(?!-c[ea]).+)/,\n  rmargin = /^margin/,\n  rnumsplit = new RegExp( \"^(\" + core_pnum + \")(.*)$\", \"i\" ),\n  rnumnonpx = new RegExp( \"^(\" + core_pnum + \")(?!px)[a-z%]+$\", \"i\" ),\n  rrelNum = new RegExp( \"^([+-])=(\" + core_pnum + \")\", \"i\" ),\n  elemdisplay = { BODY: \"block\" },\n\n  cssShow = { position: \"absolute\", visibility: \"hidden\", display: \"block\" },\n  cssNormalTransform = {\n    letterSpacing: 0,\n    fontWeight: 400\n  },\n\n  cssExpand = [ \"Top\", \"Right\", \"Bottom\", \"Left\" ],\n  cssPrefixes = [ \"Webkit\", \"O\", \"Moz\", \"ms\" ];\n\n// return a css property mapped to a potentially vendor prefixed property\nfunction vendorPropName( style, name ) {\n\n  // shortcut for names that are not vendor prefixed\n  if ( name in style ) {\n    return name;\n  }\n\n  // check for vendor prefixed names\n  var capName = name.charAt(0).toUpperCase() + name.slice(1),\n    origName = name,\n    i = cssPrefixes.length;\n\n  while ( i-- ) {\n    name = cssPrefixes[ i ] + capName;\n    if ( name in style ) {\n      return name;\n    }\n  }\n\n  return origName;\n}\n\nfunction isHidden( elem, el ) {\n  // isHidden might be called from jQuery#filter function;\n  // in that case, element will be second argument\n  elem = el || elem;\n  return jQuery.css( elem, \"display\" ) === \"none\" || !jQuery.contains( elem.ownerDocument, elem );\n}\n\n// NOTE: we've included the \"window\" in window.getComputedStyle\n// because jsdom on node.js will break without it.\nfunction getStyles( elem ) {\n  return window.getComputedStyle( elem, null );\n}\n\nfunction showHide( elements, show ) {\n  var display, elem, hidden,\n    values = [],\n    index = 0,\n    length = elements.length;\n\n  for ( ; index < length; index++ ) {\n    elem = elements[ index ];\n    if ( !elem.style ) {\n      continue;\n    }\n\n    values[ index ] = data_priv.get( elem, \"olddisplay\" );\n    display = elem.style.display;\n    if ( show ) {\n      // Reset the inline display of this element to learn if it is\n      // being hidden by cascaded rules or not\n      if ( !values[ index ] && display === \"none\" ) {\n        elem.style.display = \"\";\n      }\n\n      // Set elements which have been overridden with display: none\n      // in a stylesheet to whatever the default browser style is\n      // for such an element\n      if ( elem.style.display === \"\" && isHidden( elem ) ) {\n        values[ index ] = data_priv.access( elem, \"olddisplay\", css_defaultDisplay(elem.nodeName) );\n      }\n    } else {\n\n      if ( !values[ index ] ) {\n        hidden = isHidden( elem );\n\n        if ( display && display !== \"none\" || !hidden ) {\n          data_priv.set( elem, \"olddisplay\", hidden ? display : jQuery.css(elem, \"display\") );\n        }\n      }\n    }\n  }\n\n  // Set the display of most of the elements in a second loop\n  // to avoid the constant reflow\n  for ( index = 0; index < length; index++ ) {\n    elem = elements[ index ];\n    if ( !elem.style ) {\n      continue;\n    }\n    if ( !show || elem.style.display === \"none\" || elem.style.display === \"\" ) {\n      elem.style.display = show ? values[ index ] || \"\" : \"none\";\n    }\n  }\n\n  return elements;\n}\n\njQuery.fn.extend({\n  css: function( name, value ) {\n    return jQuery.access( this, function( elem, name, value ) {\n      var styles, len,\n        map = {},\n        i = 0;\n\n      if ( jQuery.isArray( name ) ) {\n        styles = getStyles( elem );\n        len = name.length;\n\n        for ( ; i < len; i++ ) {\n          map[ name[ i ] ] = jQuery.css( elem, name[ i ], false, styles );\n        }\n\n        return map;\n      }\n\n      return value !== undefined ?\n        jQuery.style( elem, name, value ) :\n        jQuery.css( elem, name );\n    }, name, value, arguments.length > 1 );\n  },\n  show: function() {\n    return showHide( this, true );\n  },\n  hide: function() {\n    return showHide( this );\n  },\n  toggle: function( state ) {\n    if ( typeof state === \"boolean\" ) {\n      return state ? this.show() : this.hide();\n    }\n\n    return this.each(function() {\n      if ( isHidden( this ) ) {\n        jQuery( this ).show();\n      } else {\n        jQuery( this ).hide();\n      }\n    });\n  }\n});\n\njQuery.extend({\n  // Add in style property hooks for overriding the default\n  // behavior of getting and setting a style property\n  cssHooks: {\n    opacity: {\n      get: function( elem, computed ) {\n        if ( computed ) {\n          // We should always get a number back from opacity\n          var ret = curCSS( elem, \"opacity\" );\n          return ret === \"\" ? \"1\" : ret;\n        }\n      }\n    }\n  },\n\n  // Don't automatically add \"px\" to these possibly-unitless properties\n  cssNumber: {\n    \"columnCount\": true,\n    \"fillOpacity\": true,\n    \"fontWeight\": true,\n    \"lineHeight\": true,\n    \"opacity\": true,\n    \"order\": true,\n    \"orphans\": true,\n    \"widows\": true,\n    \"zIndex\": true,\n    \"zoom\": true\n  },\n\n  // Add in properties whose names you wish to fix before\n  // setting or getting the value\n  cssProps: {\n    // normalize float css property\n    \"float\": \"cssFloat\"\n  },\n\n  // Get and set the style property on a DOM Node\n  style: function( elem, name, value, extra ) {\n    // Don't set styles on text and comment nodes\n    if ( !elem || elem.nodeType === 3 || elem.nodeType === 8 || !elem.style ) {\n      return;\n    }\n\n    // Make sure that we're working with the right name\n    var ret, type, hooks,\n      origName = jQuery.camelCase( name ),\n      style = elem.style;\n\n    name = jQuery.cssProps[ origName ] || ( jQuery.cssProps[ origName ] = vendorPropName( style, origName ) );\n\n    // gets hook for the prefixed version\n    // followed by the unprefixed version\n    hooks = jQuery.cssHooks[ name ] || jQuery.cssHooks[ origName ];\n\n    // Check if we're setting a value\n    if ( value !== undefined ) {\n      type = typeof value;\n\n      // convert relative number strings (+= or -=) to relative numbers. #7345\n      if ( type === \"string\" && (ret = rrelNum.exec( value )) ) {\n        value = ( ret[1] + 1 ) * ret[2] + parseFloat( jQuery.css( elem, name ) );\n        // Fixes bug #9237\n        type = \"number\";\n      }\n\n      // Make sure that NaN and null values aren't set. See: #7116\n      if ( value == null || type === \"number\" && isNaN( value ) ) {\n        return;\n      }\n\n      // If a number was passed in, add 'px' to the (except for certain CSS properties)\n      if ( type === \"number\" && !jQuery.cssNumber[ origName ] ) {\n        value += \"px\";\n      }\n\n      // Fixes #8908, it can be done more correctly by specifying setters in cssHooks,\n      // but it would mean to define eight (for every problematic property) identical functions\n      if ( !jQuery.support.clearCloneStyle && value === \"\" && name.indexOf(\"background\") === 0 ) {\n        style[ name ] = \"inherit\";\n      }\n\n      // If a hook was provided, use that value, otherwise just set the specified value\n      if ( !hooks || !(\"set\" in hooks) || (value = hooks.set( elem, value, extra )) !== undefined ) {\n        style[ name ] = value;\n      }\n\n    } else {\n      // If a hook was provided get the non-computed value from there\n      if ( hooks && \"get\" in hooks && (ret = hooks.get( elem, false, extra )) !== undefined ) {\n        return ret;\n      }\n\n      // Otherwise just get the value from the style object\n      return style[ name ];\n    }\n  },\n\n  css: function( elem, name, extra, styles ) {\n    var val, num, hooks,\n      origName = jQuery.camelCase( name );\n\n    // Make sure that we're working with the right name\n    name = jQuery.cssProps[ origName ] || ( jQuery.cssProps[ origName ] = vendorPropName( elem.style, origName ) );\n\n    // gets hook for the prefixed version\n    // followed by the unprefixed version\n    hooks = jQuery.cssHooks[ name ] || jQuery.cssHooks[ origName ];\n\n    // If a hook was provided get the computed value from there\n    if ( hooks && \"get\" in hooks ) {\n      val = hooks.get( elem, true, extra );\n    }\n\n    // Otherwise, if a way to get the computed value exists, use that\n    if ( val === undefined ) {\n      val = curCSS( elem, name, styles );\n    }\n\n    //convert \"normal\" to computed value\n    if ( val === \"normal\" && name in cssNormalTransform ) {\n      val = cssNormalTransform[ name ];\n    }\n\n    // Return, converting to number if forced or a qualifier was provided and val looks numeric\n    if ( extra === \"\" || extra ) {\n      num = parseFloat( val );\n      return extra === true || jQuery.isNumeric( num ) ? num || 0 : val;\n    }\n    return val;\n  }\n});\n\ncurCSS = function( elem, name, _computed ) {\n  var width, minWidth, maxWidth,\n    computed = _computed || getStyles( elem ),\n\n    // Support: IE9\n    // getPropertyValue is only needed for .css('filter') in IE9, see #12537\n    ret = computed ? computed.getPropertyValue( name ) || computed[ name ] : undefined,\n    style = elem.style;\n\n  if ( computed ) {\n\n    if ( ret === \"\" && !jQuery.contains( elem.ownerDocument, elem ) ) {\n      ret = jQuery.style( elem, name );\n    }\n\n    // Support: Safari 5.1\n    // A tribute to the \"awesome hack by Dean Edwards\"\n    // Safari 5.1.7 (at least) returns percentage for a larger set of values, but width seems to be reliably pixels\n    // this is against the CSSOM draft spec: http://dev.w3.org/csswg/cssom/#resolved-values\n    if ( rnumnonpx.test( ret ) && rmargin.test( name ) ) {\n\n      // Remember the original values\n      width = style.width;\n      minWidth = style.minWidth;\n      maxWidth = style.maxWidth;\n\n      // Put in the new values to get a computed value out\n      style.minWidth = style.maxWidth = style.width = ret;\n      ret = computed.width;\n\n      // Revert the changed values\n      style.width = width;\n      style.minWidth = minWidth;\n      style.maxWidth = maxWidth;\n    }\n  }\n\n  return ret;\n};\n\n\nfunction setPositiveNumber( elem, value, subtract ) {\n  var matches = rnumsplit.exec( value );\n  return matches ?\n    // Guard against undefined \"subtract\", e.g., when used as in cssHooks\n    Math.max( 0, matches[ 1 ] - ( subtract || 0 ) ) + ( matches[ 2 ] || \"px\" ) :\n    value;\n}\n\nfunction augmentWidthOrHeight( elem, name, extra, isBorderBox, styles ) {\n  var i = extra === ( isBorderBox ? \"border\" : \"content\" ) ?\n    // If we already have the right measurement, avoid augmentation\n    4 :\n    // Otherwise initialize for horizontal or vertical properties\n    name === \"width\" ? 1 : 0,\n\n    val = 0;\n\n  for ( ; i < 4; i += 2 ) {\n    // both box models exclude margin, so add it if we want it\n    if ( extra === \"margin\" ) {\n      val += jQuery.css( elem, extra + cssExpand[ i ], true, styles );\n    }\n\n    if ( isBorderBox ) {\n      // border-box includes padding, so remove it if we want content\n      if ( extra === \"content\" ) {\n        val -= jQuery.css( elem, \"padding\" + cssExpand[ i ], true, styles );\n      }\n\n      // at this point, extra isn't border nor margin, so remove border\n      if ( extra !== \"margin\" ) {\n        val -= jQuery.css( elem, \"border\" + cssExpand[ i ] + \"Width\", true, styles );\n      }\n    } else {\n      // at this point, extra isn't content, so add padding\n      val += jQuery.css( elem, \"padding\" + cssExpand[ i ], true, styles );\n\n      // at this point, extra isn't content nor padding, so add border\n      if ( extra !== \"padding\" ) {\n        val += jQuery.css( elem, \"border\" + cssExpand[ i ] + \"Width\", true, styles );\n      }\n    }\n  }\n\n  return val;\n}\n\nfunction getWidthOrHeight( elem, name, extra ) {\n\n  // Start with offset property, which is equivalent to the border-box value\n  var valueIsBorderBox = true,\n    val = name === \"width\" ? elem.offsetWidth : elem.offsetHeight,\n    styles = getStyles( elem ),\n    isBorderBox = jQuery.support.boxSizing && jQuery.css( elem, \"boxSizing\", false, styles ) === \"border-box\";\n\n  // some non-html elements return undefined for offsetWidth, so check for null/undefined\n  // svg - https://bugzilla.mozilla.org/show_bug.cgi?id=649285\n  // MathML - https://bugzilla.mozilla.org/show_bug.cgi?id=491668\n  if ( val <= 0 || val == null ) {\n    // Fall back to computed then uncomputed css if necessary\n    val = curCSS( elem, name, styles );\n    if ( val < 0 || val == null ) {\n      val = elem.style[ name ];\n    }\n\n    // Computed unit is not pixels. Stop here and return.\n    if ( rnumnonpx.test(val) ) {\n      return val;\n    }\n\n    // we need the check for style in case a browser which returns unreliable values\n    // for getComputedStyle silently falls back to the reliable elem.style\n    valueIsBorderBox = isBorderBox && ( jQuery.support.boxSizingReliable || val === elem.style[ name ] );\n\n    // Normalize \"\", auto, and prepare for extra\n    val = parseFloat( val ) || 0;\n  }\n\n  // use the active box-sizing model to add/subtract irrelevant styles\n  return ( val +\n    augmentWidthOrHeight(\n      elem,\n      name,\n      extra || ( isBorderBox ? \"border\" : \"content\" ),\n      valueIsBorderBox,\n      styles\n    )\n  ) + \"px\";\n}\n\n// Try to determine the default display value of an element\nfunction css_defaultDisplay( nodeName ) {\n  var doc = document,\n    display = elemdisplay[ nodeName ];\n\n  if ( !display ) {\n    display = actualDisplay( nodeName, doc );\n\n    // If the simple way fails, read from inside an iframe\n    if ( display === \"none\" || !display ) {\n      // Use the already-created iframe if possible\n      iframe = ( iframe ||\n        jQuery(\"<iframe frameborder='0' width='0' height='0'/>\")\n        .css( \"cssText\", \"display:block !important\" )\n      ).appendTo( doc.documentElement );\n\n      // Always write a new HTML skeleton so Webkit and Firefox don't choke on reuse\n      doc = ( iframe[0].contentWindow || iframe[0].contentDocument ).document;\n      doc.write(\"<!doctype html><html><body>\");\n      doc.close();\n\n      display = actualDisplay( nodeName, doc );\n      iframe.detach();\n    }\n\n    // Store the correct default display\n    elemdisplay[ nodeName ] = display;\n  }\n\n  return display;\n}\n\n// Called ONLY from within css_defaultDisplay\nfunction actualDisplay( name, doc ) {\n  var elem = jQuery( doc.createElement( name ) ).appendTo( doc.body ),\n    display = jQuery.css( elem[0], \"display\" );\n  elem.remove();\n  return display;\n}\n\njQuery.each([ \"height\", \"width\" ], function( i, name ) {\n  jQuery.cssHooks[ name ] = {\n    get: function( elem, computed, extra ) {\n      if ( computed ) {\n        // certain elements can have dimension info if we invisibly show them\n        // however, it must have a current display style that would benefit from this\n        return elem.offsetWidth === 0 && rdisplayswap.test( jQuery.css( elem, \"display\" ) ) ?\n          jQuery.swap( elem, cssShow, function() {\n            return getWidthOrHeight( elem, name, extra );\n          }) :\n          getWidthOrHeight( elem, name, extra );\n      }\n    },\n\n    set: function( elem, value, extra ) {\n      var styles = extra && getStyles( elem );\n      return setPositiveNumber( elem, value, extra ?\n        augmentWidthOrHeight(\n          elem,\n          name,\n          extra,\n          jQuery.support.boxSizing && jQuery.css( elem, \"boxSizing\", false, styles ) === \"border-box\",\n          styles\n        ) : 0\n      );\n    }\n  };\n});\n\n// These hooks cannot be added until DOM ready because the support test\n// for it is not run until after DOM ready\njQuery(function() {\n  // Support: Android 2.3\n  if ( !jQuery.support.reliableMarginRight ) {\n    jQuery.cssHooks.marginRight = {\n      get: function( elem, computed ) {\n        if ( computed ) {\n          // Support: Android 2.3\n          // WebKit Bug 13343 - getComputedStyle returns wrong value for margin-right\n          // Work around by temporarily setting element display to inline-block\n          return jQuery.swap( elem, { \"display\": \"inline-block\" },\n            curCSS, [ elem, \"marginRight\" ] );\n        }\n      }\n    };\n  }\n\n  // Webkit bug: https://bugs.webkit.org/show_bug.cgi?id=29084\n  // getComputedStyle returns percent when specified for top/left/bottom/right\n  // rather than make the css module depend on the offset module, we just check for it here\n  if ( !jQuery.support.pixelPosition && jQuery.fn.position ) {\n    jQuery.each( [ \"top\", \"left\" ], function( i, prop ) {\n      jQuery.cssHooks[ prop ] = {\n        get: function( elem, computed ) {\n          if ( computed ) {\n            computed = curCSS( elem, prop );\n            // if curCSS returns percentage, fallback to offset\n            return rnumnonpx.test( computed ) ?\n              jQuery( elem ).position()[ prop ] + \"px\" :\n              computed;\n          }\n        }\n      };\n    });\n  }\n\n});\n\nif ( jQuery.expr && jQuery.expr.filters ) {\n  jQuery.expr.filters.hidden = function( elem ) {\n    // Support: Opera <= 12.12\n    // Opera reports offsetWidths and offsetHeights less than zero on some elements\n    return elem.offsetWidth <= 0 && elem.offsetHeight <= 0;\n  };\n\n  jQuery.expr.filters.visible = function( elem ) {\n    return !jQuery.expr.filters.hidden( elem );\n  };\n}\n\n// These hooks are used by animate to expand properties\njQuery.each({\n  margin: \"\",\n  padding: \"\",\n  border: \"Width\"\n}, function( prefix, suffix ) {\n  jQuery.cssHooks[ prefix + suffix ] = {\n    expand: function( value ) {\n      var i = 0,\n        expanded = {},\n\n        // assumes a single number if not a string\n        parts = typeof value === \"string\" ? value.split(\" \") : [ value ];\n\n      for ( ; i < 4; i++ ) {\n        expanded[ prefix + cssExpand[ i ] + suffix ] =\n          parts[ i ] || parts[ i - 2 ] || parts[ 0 ];\n      }\n\n      return expanded;\n    }\n  };\n\n  if ( !rmargin.test( prefix ) ) {\n    jQuery.cssHooks[ prefix + suffix ].set = setPositiveNumber;\n  }\n});\nvar r20 = /%20/g,\n  rbracket = /\\[\\]$/,\n  rCRLF = /\\r?\\n/g,\n  rsubmitterTypes = /^(?:submit|button|image|reset|file)$/i,\n  rsubmittable = /^(?:input|select|textarea|keygen)/i;\n\njQuery.fn.extend({\n  serialize: function() {\n    return jQuery.param( this.serializeArray() );\n  },\n  serializeArray: function() {\n    return this.map(function(){\n      // Can add propHook for \"elements\" to filter or add form elements\n      var elements = jQuery.prop( this, \"elements\" );\n      return elements ? jQuery.makeArray( elements ) : this;\n    })\n    .filter(function(){\n      var type = this.type;\n      // Use .is(\":disabled\") so that fieldset[disabled] works\n      return this.name && !jQuery( this ).is( \":disabled\" ) &&\n        rsubmittable.test( this.nodeName ) && !rsubmitterTypes.test( type ) &&\n        ( this.checked || !manipulation_rcheckableType.test( type ) );\n    })\n    .map(function( i, elem ){\n      var val = jQuery( this ).val();\n\n      return val == null ?\n        null :\n        jQuery.isArray( val ) ?\n          jQuery.map( val, function( val ){\n            return { name: elem.name, value: val.replace( rCRLF, \"\\r\\n\" ) };\n          }) :\n          { name: elem.name, value: val.replace( rCRLF, \"\\r\\n\" ) };\n    }).get();\n  }\n});\n\n//Serialize an array of form elements or a set of\n//key/values into a query string\njQuery.param = function( a, traditional ) {\n  var prefix,\n    s = [],\n    add = function( key, value ) {\n      // If value is a function, invoke it and return its value\n      value = jQuery.isFunction( value ) ? value() : ( value == null ? \"\" : value );\n      s[ s.length ] = encodeURIComponent( key ) + \"=\" + encodeURIComponent( value );\n    };\n\n  // Set traditional to true for jQuery <= 1.3.2 behavior.\n  if ( traditional === undefined ) {\n    traditional = jQuery.ajaxSettings && jQuery.ajaxSettings.traditional;\n  }\n\n  // If an array was passed in, assume that it is an array of form elements.\n  if ( jQuery.isArray( a ) || ( a.jquery && !jQuery.isPlainObject( a ) ) ) {\n    // Serialize the form elements\n    jQuery.each( a, function() {\n      add( this.name, this.value );\n    });\n\n  } else {\n    // If traditional, encode the \"old\" way (the way 1.3.2 or older\n    // did it), otherwise encode params recursively.\n    for ( prefix in a ) {\n      buildParams( prefix, a[ prefix ], traditional, add );\n    }\n  }\n\n  // Return the resulting serialization\n  return s.join( \"&\" ).replace( r20, \"+\" );\n};\n\nfunction buildParams( prefix, obj, traditional, add ) {\n  var name;\n\n  if ( jQuery.isArray( obj ) ) {\n    // Serialize array item.\n    jQuery.each( obj, function( i, v ) {\n      if ( traditional || rbracket.test( prefix ) ) {\n        // Treat each array item as a scalar.\n        add( prefix, v );\n\n      } else {\n        // Item is non-scalar (array or object), encode its numeric index.\n        buildParams( prefix + \"[\" + ( typeof v === \"object\" ? i : \"\" ) + \"]\", v, traditional, add );\n      }\n    });\n\n  } else if ( !traditional && jQuery.type( obj ) === \"object\" ) {\n    // Serialize object item.\n    for ( name in obj ) {\n      buildParams( prefix + \"[\" + name + \"]\", obj[ name ], traditional, add );\n    }\n\n  } else {\n    // Serialize scalar item.\n    add( prefix, obj );\n  }\n}\njQuery.each( (\"blur focus focusin focusout load resize scroll unload click dblclick \" +\n  \"mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave \" +\n  \"change select submit keydown keypress keyup error contextmenu\").split(\" \"), function( i, name ) {\n\n  // Handle event binding\n  jQuery.fn[ name ] = function( data, fn ) {\n    return arguments.length > 0 ?\n      this.on( name, null, data, fn ) :\n      this.trigger( name );\n  };\n});\n\njQuery.fn.extend({\n  hover: function( fnOver, fnOut ) {\n    return this.mouseenter( fnOver ).mouseleave( fnOut || fnOver );\n  },\n\n  bind: function( types, data, fn ) {\n    return this.on( types, null, data, fn );\n  },\n  unbind: function( types, fn ) {\n    return this.off( types, null, fn );\n  },\n\n  delegate: function( selector, types, data, fn ) {\n    return this.on( types, selector, data, fn );\n  },\n  undelegate: function( selector, types, fn ) {\n    // ( namespace ) or ( selector, types [, fn] )\n    return arguments.length === 1 ? this.off( selector, \"**\" ) : this.off( types, selector || \"**\", fn );\n  }\n});\nvar\n  // Document location\n  ajaxLocParts,\n  ajaxLocation,\n\n  ajax_nonce = jQuery.now(),\n\n  ajax_rquery = /\\?/,\n  rhash = /#.*$/,\n  rts = /([?&])_=[^&]*/,\n  rheaders = /^(.*?):[ \\t]*([^\\r\\n]*)$/mg,\n  // #7653, #8125, #8152: local protocol detection\n  rlocalProtocol = /^(?:about|app|app-storage|.+-extension|file|res|widget):$/,\n  rnoContent = /^(?:GET|HEAD)$/,\n  rprotocol = /^\\/\\//,\n  rurl = /^([\\w.+-]+:)(?:\\/\\/([^\\/?#:]*)(?::(\\d+)|)|)/,\n\n  // Keep a copy of the old load method\n  _load = jQuery.fn.load,\n\n  /* Prefilters\n   * 1) They are useful to introduce custom dataTypes (see ajax/jsonp.js for an example)\n   * 2) These are called:\n   *    - BEFORE asking for a transport\n   *    - AFTER param serialization (s.data is a string if s.processData is true)\n   * 3) key is the dataType\n   * 4) the catchall symbol \"*\" can be used\n   * 5) execution will start with transport dataType and THEN continue down to \"*\" if needed\n   */\n  prefilters = {},\n\n  /* Transports bindings\n   * 1) key is the dataType\n   * 2) the catchall symbol \"*\" can be used\n   * 3) selection will start with transport dataType and THEN go to \"*\" if needed\n   */\n  transports = {},\n\n  // Avoid comment-prolog char sequence (#10098); must appease lint and evade compression\n  allTypes = \"*/\".concat(\"*\");\n\n// #8138, IE may throw an exception when accessing\n// a field from window.location if document.domain has been set\ntry {\n  ajaxLocation = location.href;\n} catch( e ) {\n  // Use the href attribute of an A element\n  // since IE will modify it given document.location\n  ajaxLocation = document.createElement( \"a\" );\n  ajaxLocation.href = \"\";\n  ajaxLocation = ajaxLocation.href;\n}\n\n// Segment location into parts\najaxLocParts = rurl.exec( ajaxLocation.toLowerCase() ) || [];\n\n// Base \"constructor\" for jQuery.ajaxPrefilter and jQuery.ajaxTransport\nfunction addToPrefiltersOrTransports( structure ) {\n\n  // dataTypeExpression is optional and defaults to \"*\"\n  return function( dataTypeExpression, func ) {\n\n    if ( typeof dataTypeExpression !== \"string\" ) {\n      func = dataTypeExpression;\n      dataTypeExpression = \"*\";\n    }\n\n    var dataType,\n      i = 0,\n      dataTypes = dataTypeExpression.toLowerCase().match( core_rnotwhite ) || [];\n\n    if ( jQuery.isFunction( func ) ) {\n      // For each dataType in the dataTypeExpression\n      while ( (dataType = dataTypes[i++]) ) {\n        // Prepend if requested\n        if ( dataType[0] === \"+\" ) {\n          dataType = dataType.slice( 1 ) || \"*\";\n          (structure[ dataType ] = structure[ dataType ] || []).unshift( func );\n\n        // Otherwise append\n        } else {\n          (structure[ dataType ] = structure[ dataType ] || []).push( func );\n        }\n      }\n    }\n  };\n}\n\n// Base inspection function for prefilters and transports\nfunction inspectPrefiltersOrTransports( structure, options, originalOptions, jqXHR ) {\n\n  var inspected = {},\n    seekingTransport = ( structure === transports );\n\n  function inspect( dataType ) {\n    var selected;\n    inspected[ dataType ] = true;\n    jQuery.each( structure[ dataType ] || [], function( _, prefilterOrFactory ) {\n      var dataTypeOrTransport = prefilterOrFactory( options, originalOptions, jqXHR );\n      if( typeof dataTypeOrTransport === \"string\" && !seekingTransport && !inspected[ dataTypeOrTransport ] ) {\n        options.dataTypes.unshift( dataTypeOrTransport );\n        inspect( dataTypeOrTransport );\n        return false;\n      } else if ( seekingTransport ) {\n        return !( selected = dataTypeOrTransport );\n      }\n    });\n    return selected;\n  }\n\n  return inspect( options.dataTypes[ 0 ] ) || !inspected[ \"*\" ] && inspect( \"*\" );\n}\n\n// A special extend for ajax options\n// that takes \"flat\" options (not to be deep extended)\n// Fixes #9887\nfunction ajaxExtend( target, src ) {\n  var key, deep,\n    flatOptions = jQuery.ajaxSettings.flatOptions || {};\n\n  for ( key in src ) {\n    if ( src[ key ] !== undefined ) {\n      ( flatOptions[ key ] ? target : ( deep || (deep = {}) ) )[ key ] = src[ key ];\n    }\n  }\n  if ( deep ) {\n    jQuery.extend( true, target, deep );\n  }\n\n  return target;\n}\n\njQuery.fn.load = function( url, params, callback ) {\n  if ( typeof url !== \"string\" && _load ) {\n    return _load.apply( this, arguments );\n  }\n\n  var selector, type, response,\n    self = this,\n    off = url.indexOf(\" \");\n\n  if ( off >= 0 ) {\n    selector = url.slice( off );\n    url = url.slice( 0, off );\n  }\n\n  // If it's a function\n  if ( jQuery.isFunction( params ) ) {\n\n    // We assume that it's the callback\n    callback = params;\n    params = undefined;\n\n  // Otherwise, build a param string\n  } else if ( params && typeof params === \"object\" ) {\n    type = \"POST\";\n  }\n\n  // If we have elements to modify, make the request\n  if ( self.length > 0 ) {\n    jQuery.ajax({\n      url: url,\n\n      // if \"type\" variable is undefined, then \"GET\" method will be used\n      type: type,\n      dataType: \"html\",\n      data: params\n    }).done(function( responseText ) {\n\n      // Save response for use in complete callback\n      response = arguments;\n\n      self.html( selector ?\n\n        // If a selector was specified, locate the right elements in a dummy div\n        // Exclude scripts to avoid IE 'Permission Denied' errors\n        jQuery(\"<div>\").append( jQuery.parseHTML( responseText ) ).find( selector ) :\n\n        // Otherwise use the full result\n        responseText );\n\n    }).complete( callback && function( jqXHR, status ) {\n      self.each( callback, response || [ jqXHR.responseText, status, jqXHR ] );\n    });\n  }\n\n  return this;\n};\n\n// Attach a bunch of functions for handling common AJAX events\njQuery.each( [ \"ajaxStart\", \"ajaxStop\", \"ajaxComplete\", \"ajaxError\", \"ajaxSuccess\", \"ajaxSend\" ], function( i, type ){\n  jQuery.fn[ type ] = function( fn ){\n    return this.on( type, fn );\n  };\n});\n\njQuery.extend({\n\n  // Counter for holding the number of active queries\n  active: 0,\n\n  // Last-Modified header cache for next request\n  lastModified: {},\n  etag: {},\n\n  ajaxSettings: {\n    url: ajaxLocation,\n    type: \"GET\",\n    isLocal: rlocalProtocol.test( ajaxLocParts[ 1 ] ),\n    global: true,\n    processData: true,\n    async: true,\n    contentType: \"application/x-www-form-urlencoded; charset=UTF-8\",\n    /*\n    timeout: 0,\n    data: null,\n    dataType: null,\n    username: null,\n    password: null,\n    cache: null,\n    throws: false,\n    traditional: false,\n    headers: {},\n    */\n\n    accepts: {\n      \"*\": allTypes,\n      text: \"text/plain\",\n      html: \"text/html\",\n      xml: \"application/xml, text/xml\",\n      json: \"application/json, text/javascript\"\n    },\n\n    contents: {\n      xml: /xml/,\n      html: /html/,\n      json: /json/\n    },\n\n    responseFields: {\n      xml: \"responseXML\",\n      text: \"responseText\",\n      json: \"responseJSON\"\n    },\n\n    // Data converters\n    // Keys separate source (or catchall \"*\") and destination types with a single space\n    converters: {\n\n      // Convert anything to text\n      \"* text\": String,\n\n      // Text to html (true = no transformation)\n      \"text html\": true,\n\n      // Evaluate text as a json expression\n      \"text json\": jQuery.parseJSON,\n\n      // Parse text as xml\n      \"text xml\": jQuery.parseXML\n    },\n\n    // For options that shouldn't be deep extended:\n    // you can add your own custom options here if\n    // and when you create one that shouldn't be\n    // deep extended (see ajaxExtend)\n    flatOptions: {\n      url: true,\n      context: true\n    }\n  },\n\n  // Creates a full fledged settings object into target\n  // with both ajaxSettings and settings fields.\n  // If target is omitted, writes into ajaxSettings.\n  ajaxSetup: function( target, settings ) {\n    return settings ?\n\n      // Building a settings object\n      ajaxExtend( ajaxExtend( target, jQuery.ajaxSettings ), settings ) :\n\n      // Extending ajaxSettings\n      ajaxExtend( jQuery.ajaxSettings, target );\n  },\n\n  ajaxPrefilter: addToPrefiltersOrTransports( prefilters ),\n  ajaxTransport: addToPrefiltersOrTransports( transports ),\n\n  // Main method\n  ajax: function( url, options ) {\n\n    // If url is an object, simulate pre-1.5 signature\n    if ( typeof url === \"object\" ) {\n      options = url;\n      url = undefined;\n    }\n\n    // Force options to be an object\n    options = options || {};\n\n    var transport,\n      // URL without anti-cache param\n      cacheURL,\n      // Response headers\n      responseHeadersString,\n      responseHeaders,\n      // timeout handle\n      timeoutTimer,\n      // Cross-domain detection vars\n      parts,\n      // To know if global events are to be dispatched\n      fireGlobals,\n      // Loop variable\n      i,\n      // Create the final options object\n      s = jQuery.ajaxSetup( {}, options ),\n      // Callbacks context\n      callbackContext = s.context || s,\n      // Context for global events is callbackContext if it is a DOM node or jQuery collection\n      globalEventContext = s.context && ( callbackContext.nodeType || callbackContext.jquery ) ?\n        jQuery( callbackContext ) :\n        jQuery.event,\n      // Deferreds\n      deferred = jQuery.Deferred(),\n      completeDeferred = jQuery.Callbacks(\"once memory\"),\n      // Status-dependent callbacks\n      statusCode = s.statusCode || {},\n      // Headers (they are sent all at once)\n      requestHeaders = {},\n      requestHeadersNames = {},\n      // The jqXHR state\n      state = 0,\n      // Default abort message\n      strAbort = \"canceled\",\n      // Fake xhr\n      jqXHR = {\n        readyState: 0,\n\n        // Builds headers hashtable if needed\n        getResponseHeader: function( key ) {\n          var match;\n          if ( state === 2 ) {\n            if ( !responseHeaders ) {\n              responseHeaders = {};\n              while ( (match = rheaders.exec( responseHeadersString )) ) {\n                responseHeaders[ match[1].toLowerCase() ] = match[ 2 ];\n              }\n            }\n            match = responseHeaders[ key.toLowerCase() ];\n          }\n          return match == null ? null : match;\n        },\n\n        // Raw string\n        getAllResponseHeaders: function() {\n          return state === 2 ? responseHeadersString : null;\n        },\n\n        // Caches the header\n        setRequestHeader: function( name, value ) {\n          var lname = name.toLowerCase();\n          if ( !state ) {\n            name = requestHeadersNames[ lname ] = requestHeadersNames[ lname ] || name;\n            requestHeaders[ name ] = value;\n          }\n          return this;\n        },\n\n        // Overrides response content-type header\n        overrideMimeType: function( type ) {\n          if ( !state ) {\n            s.mimeType = type;\n          }\n          return this;\n        },\n\n        // Status-dependent callbacks\n        statusCode: function( map ) {\n          var code;\n          if ( map ) {\n            if ( state < 2 ) {\n              for ( code in map ) {\n                // Lazy-add the new callback in a way that preserves old ones\n                statusCode[ code ] = [ statusCode[ code ], map[ code ] ];\n              }\n            } else {\n              // Execute the appropriate callbacks\n              jqXHR.always( map[ jqXHR.status ] );\n            }\n          }\n          return this;\n        },\n\n        // Cancel the request\n        abort: function( statusText ) {\n          var finalText = statusText || strAbort;\n          if ( transport ) {\n            transport.abort( finalText );\n          }\n          done( 0, finalText );\n          return this;\n        }\n      };\n\n    // Attach deferreds\n    deferred.promise( jqXHR ).complete = completeDeferred.add;\n    jqXHR.success = jqXHR.done;\n    jqXHR.error = jqXHR.fail;\n\n    // Remove hash character (#7531: and string promotion)\n    // Add protocol if not provided (prefilters might expect it)\n    // Handle falsy url in the settings object (#10093: consistency with old signature)\n    // We also use the url parameter if available\n    s.url = ( ( url || s.url || ajaxLocation ) + \"\" ).replace( rhash, \"\" )\n      .replace( rprotocol, ajaxLocParts[ 1 ] + \"//\" );\n\n    // Alias method option to type as per ticket #12004\n    s.type = options.method || options.type || s.method || s.type;\n\n    // Extract dataTypes list\n    s.dataTypes = jQuery.trim( s.dataType || \"*\" ).toLowerCase().match( core_rnotwhite ) || [\"\"];\n\n    // A cross-domain request is in order when we have a protocol:host:port mismatch\n    if ( s.crossDomain == null ) {\n      parts = rurl.exec( s.url.toLowerCase() );\n      s.crossDomain = !!( parts &&\n        ( parts[ 1 ] !== ajaxLocParts[ 1 ] || parts[ 2 ] !== ajaxLocParts[ 2 ] ||\n          ( parts[ 3 ] || ( parts[ 1 ] === \"http:\" ? \"80\" : \"443\" ) ) !==\n            ( ajaxLocParts[ 3 ] || ( ajaxLocParts[ 1 ] === \"http:\" ? \"80\" : \"443\" ) ) )\n      );\n    }\n\n    // Convert data if not already a string\n    if ( s.data && s.processData && typeof s.data !== \"string\" ) {\n      s.data = jQuery.param( s.data, s.traditional );\n    }\n\n    // Apply prefilters\n    inspectPrefiltersOrTransports( prefilters, s, options, jqXHR );\n\n    // If request was aborted inside a prefilter, stop there\n    if ( state === 2 ) {\n      return jqXHR;\n    }\n\n    // We can fire global events as of now if asked to\n    fireGlobals = s.global;\n\n    // Watch for a new set of requests\n    if ( fireGlobals && jQuery.active++ === 0 ) {\n      jQuery.event.trigger(\"ajaxStart\");\n    }\n\n    // Uppercase the type\n    s.type = s.type.toUpperCase();\n\n    // Determine if request has content\n    s.hasContent = !rnoContent.test( s.type );\n\n    // Save the URL in case we're toying with the If-Modified-Since\n    // and/or If-None-Match header later on\n    cacheURL = s.url;\n\n    // More options handling for requests with no content\n    if ( !s.hasContent ) {\n\n      // If data is available, append data to url\n      if ( s.data ) {\n        cacheURL = ( s.url += ( ajax_rquery.test( cacheURL ) ? \"&\" : \"?\" ) + s.data );\n        // #9682: remove data so that it's not used in an eventual retry\n        delete s.data;\n      }\n\n      // Add anti-cache in url if needed\n      if ( s.cache === false ) {\n        s.url = rts.test( cacheURL ) ?\n\n          // If there is already a '_' parameter, set its value\n          cacheURL.replace( rts, \"$1_=\" + ajax_nonce++ ) :\n\n          // Otherwise add one to the end\n          cacheURL + ( ajax_rquery.test( cacheURL ) ? \"&\" : \"?\" ) + \"_=\" + ajax_nonce++;\n      }\n    }\n\n    // Set the If-Modified-Since and/or If-None-Match header, if in ifModified mode.\n    if ( s.ifModified ) {\n      if ( jQuery.lastModified[ cacheURL ] ) {\n        jqXHR.setRequestHeader( \"If-Modified-Since\", jQuery.lastModified[ cacheURL ] );\n      }\n      if ( jQuery.etag[ cacheURL ] ) {\n        jqXHR.setRequestHeader( \"If-None-Match\", jQuery.etag[ cacheURL ] );\n      }\n    }\n\n    // Set the correct header, if data is being sent\n    if ( s.data && s.hasContent && s.contentType !== false || options.contentType ) {\n      jqXHR.setRequestHeader( \"Content-Type\", s.contentType );\n    }\n\n    // Set the Accepts header for the server, depending on the dataType\n    jqXHR.setRequestHeader(\n      \"Accept\",\n      s.dataTypes[ 0 ] && s.accepts[ s.dataTypes[0] ] ?\n        s.accepts[ s.dataTypes[0] ] + ( s.dataTypes[ 0 ] !== \"*\" ? \", \" + allTypes + \"; q=0.01\" : \"\" ) :\n        s.accepts[ \"*\" ]\n    );\n\n    // Check for headers option\n    for ( i in s.headers ) {\n      jqXHR.setRequestHeader( i, s.headers[ i ] );\n    }\n\n    // Allow custom headers/mimetypes and early abort\n    if ( s.beforeSend && ( s.beforeSend.call( callbackContext, jqXHR, s ) === false || state === 2 ) ) {\n      // Abort if not done already and return\n      return jqXHR.abort();\n    }\n\n    // aborting is no longer a cancellation\n    strAbort = \"abort\";\n\n    // Install callbacks on deferreds\n    for ( i in { success: 1, error: 1, complete: 1 } ) {\n      jqXHR[ i ]( s[ i ] );\n    }\n\n    // Get transport\n    transport = inspectPrefiltersOrTransports( transports, s, options, jqXHR );\n\n    // If no transport, we auto-abort\n    if ( !transport ) {\n      done( -1, \"No Transport\" );\n    } else {\n      jqXHR.readyState = 1;\n\n      // Send global event\n      if ( fireGlobals ) {\n        globalEventContext.trigger( \"ajaxSend\", [ jqXHR, s ] );\n      }\n      // Timeout\n      if ( s.async && s.timeout > 0 ) {\n        timeoutTimer = setTimeout(function() {\n          jqXHR.abort(\"timeout\");\n        }, s.timeout );\n      }\n\n      try {\n        state = 1;\n        transport.send( requestHeaders, done );\n      } catch ( e ) {\n        // Propagate exception as error if not done\n        if ( state < 2 ) {\n          done( -1, e );\n        // Simply rethrow otherwise\n        } else {\n          throw e;\n        }\n      }\n    }\n\n    // Callback for when everything is done\n    function done( status, nativeStatusText, responses, headers ) {\n      var isSuccess, success, error, response, modified,\n        statusText = nativeStatusText;\n\n      // Called once\n      if ( state === 2 ) {\n        return;\n      }\n\n      // State is \"done\" now\n      state = 2;\n\n      // Clear timeout if it exists\n      if ( timeoutTimer ) {\n        clearTimeout( timeoutTimer );\n      }\n\n      // Dereference transport for early garbage collection\n      // (no matter how long the jqXHR object will be used)\n      transport = undefined;\n\n      // Cache response headers\n      responseHeadersString = headers || \"\";\n\n      // Set readyState\n      jqXHR.readyState = status > 0 ? 4 : 0;\n\n      // Determine if successful\n      isSuccess = status >= 200 && status < 300 || status === 304;\n\n      // Get response data\n      if ( responses ) {\n        response = ajaxHandleResponses( s, jqXHR, responses );\n      }\n\n      // Convert no matter what (that way responseXXX fields are always set)\n      response = ajaxConvert( s, response, jqXHR, isSuccess );\n\n      // If successful, handle type chaining\n      if ( isSuccess ) {\n\n        // Set the If-Modified-Since and/or If-None-Match header, if in ifModified mode.\n        if ( s.ifModified ) {\n          modified = jqXHR.getResponseHeader(\"Last-Modified\");\n          if ( modified ) {\n            jQuery.lastModified[ cacheURL ] = modified;\n          }\n          modified = jqXHR.getResponseHeader(\"etag\");\n          if ( modified ) {\n            jQuery.etag[ cacheURL ] = modified;\n          }\n        }\n\n        // if no content\n        if ( status === 204 || s.type === \"HEAD\" ) {\n          statusText = \"nocontent\";\n\n        // if not modified\n        } else if ( status === 304 ) {\n          statusText = \"notmodified\";\n\n        // If we have data, let's convert it\n        } else {\n          statusText = response.state;\n          success = response.data;\n          error = response.error;\n          isSuccess = !error;\n        }\n      } else {\n        // We extract error from statusText\n        // then normalize statusText and status for non-aborts\n        error = statusText;\n        if ( status || !statusText ) {\n          statusText = \"error\";\n          if ( status < 0 ) {\n            status = 0;\n          }\n        }\n      }\n\n      // Set data for the fake xhr object\n      jqXHR.status = status;\n      jqXHR.statusText = ( nativeStatusText || statusText ) + \"\";\n\n      // Success/Error\n      if ( isSuccess ) {\n        deferred.resolveWith( callbackContext, [ success, statusText, jqXHR ] );\n      } else {\n        deferred.rejectWith( callbackContext, [ jqXHR, statusText, error ] );\n      }\n\n      // Status-dependent callbacks\n      jqXHR.statusCode( statusCode );\n      statusCode = undefined;\n\n      if ( fireGlobals ) {\n        globalEventContext.trigger( isSuccess ? \"ajaxSuccess\" : \"ajaxError\",\n          [ jqXHR, s, isSuccess ? success : error ] );\n      }\n\n      // Complete\n      completeDeferred.fireWith( callbackContext, [ jqXHR, statusText ] );\n\n      if ( fireGlobals ) {\n        globalEventContext.trigger( \"ajaxComplete\", [ jqXHR, s ] );\n        // Handle the global AJAX counter\n        if ( !( --jQuery.active ) ) {\n          jQuery.event.trigger(\"ajaxStop\");\n        }\n      }\n    }\n\n    return jqXHR;\n  },\n\n  getJSON: function( url, data, callback ) {\n    return jQuery.get( url, data, callback, \"json\" );\n  },\n\n  getScript: function( url, callback ) {\n    return jQuery.get( url, undefined, callback, \"script\" );\n  }\n});\n\njQuery.each( [ \"get\", \"post\" ], function( i, method ) {\n  jQuery[ method ] = function( url, data, callback, type ) {\n    // shift arguments if data argument was omitted\n    if ( jQuery.isFunction( data ) ) {\n      type = type || callback;\n      callback = data;\n      data = undefined;\n    }\n\n    return jQuery.ajax({\n      url: url,\n      type: method,\n      dataType: type,\n      data: data,\n      success: callback\n    });\n  };\n});\n\n/* Handles responses to an ajax request:\n * - finds the right dataType (mediates between content-type and expected dataType)\n * - returns the corresponding response\n */\nfunction ajaxHandleResponses( s, jqXHR, responses ) {\n\n  var ct, type, finalDataType, firstDataType,\n    contents = s.contents,\n    dataTypes = s.dataTypes;\n\n  // Remove auto dataType and get content-type in the process\n  while( dataTypes[ 0 ] === \"*\" ) {\n    dataTypes.shift();\n    if ( ct === undefined ) {\n      ct = s.mimeType || jqXHR.getResponseHeader(\"Content-Type\");\n    }\n  }\n\n  // Check if we're dealing with a known content-type\n  if ( ct ) {\n    for ( type in contents ) {\n      if ( contents[ type ] && contents[ type ].test( ct ) ) {\n        dataTypes.unshift( type );\n        break;\n      }\n    }\n  }\n\n  // Check to see if we have a response for the expected dataType\n  if ( dataTypes[ 0 ] in responses ) {\n    finalDataType = dataTypes[ 0 ];\n  } else {\n    // Try convertible dataTypes\n    for ( type in responses ) {\n      if ( !dataTypes[ 0 ] || s.converters[ type + \" \" + dataTypes[0] ] ) {\n        finalDataType = type;\n        break;\n      }\n      if ( !firstDataType ) {\n        firstDataType = type;\n      }\n    }\n    // Or just use first one\n    finalDataType = finalDataType || firstDataType;\n  }\n\n  // If we found a dataType\n  // We add the dataType to the list if needed\n  // and return the corresponding response\n  if ( finalDataType ) {\n    if ( finalDataType !== dataTypes[ 0 ] ) {\n      dataTypes.unshift( finalDataType );\n    }\n    return responses[ finalDataType ];\n  }\n}\n\n/* Chain conversions given the request and the original response\n * Also sets the responseXXX fields on the jqXHR instance\n */\nfunction ajaxConvert( s, response, jqXHR, isSuccess ) {\n  var conv2, current, conv, tmp, prev,\n    converters = {},\n    // Work with a copy of dataTypes in case we need to modify it for conversion\n    dataTypes = s.dataTypes.slice();\n\n  // Create converters map with lowercased keys\n  if ( dataTypes[ 1 ] ) {\n    for ( conv in s.converters ) {\n      converters[ conv.toLowerCase() ] = s.converters[ conv ];\n    }\n  }\n\n  current = dataTypes.shift();\n\n  // Convert to each sequential dataType\n  while ( current ) {\n\n    if ( s.responseFields[ current ] ) {\n      jqXHR[ s.responseFields[ current ] ] = response;\n    }\n\n    // Apply the dataFilter if provided\n    if ( !prev && isSuccess && s.dataFilter ) {\n      response = s.dataFilter( response, s.dataType );\n    }\n\n    prev = current;\n    current = dataTypes.shift();\n\n    if ( current ) {\n\n    // There's only work to do if current dataType is non-auto\n      if ( current === \"*\" ) {\n\n        current = prev;\n\n      // Convert response if prev dataType is non-auto and differs from current\n      } else if ( prev !== \"*\" && prev !== current ) {\n\n        // Seek a direct converter\n        conv = converters[ prev + \" \" + current ] || converters[ \"* \" + current ];\n\n        // If none found, seek a pair\n        if ( !conv ) {\n          for ( conv2 in converters ) {\n\n            // If conv2 outputs current\n            tmp = conv2.split( \" \" );\n            if ( tmp[ 1 ] === current ) {\n\n              // If prev can be converted to accepted input\n              conv = converters[ prev + \" \" + tmp[ 0 ] ] ||\n                converters[ \"* \" + tmp[ 0 ] ];\n              if ( conv ) {\n                // Condense equivalence converters\n                if ( conv === true ) {\n                  conv = converters[ conv2 ];\n\n                // Otherwise, insert the intermediate dataType\n                } else if ( converters[ conv2 ] !== true ) {\n                  current = tmp[ 0 ];\n                  dataTypes.unshift( tmp[ 1 ] );\n                }\n                break;\n              }\n            }\n          }\n        }\n\n        // Apply converter (if not an equivalence)\n        if ( conv !== true ) {\n\n          // Unless errors are allowed to bubble, catch and return them\n          if ( conv && s[ \"throws\" ] ) {\n            response = conv( response );\n          } else {\n            try {\n              response = conv( response );\n            } catch ( e ) {\n              return { state: \"parsererror\", error: conv ? e : \"No conversion from \" + prev + \" to \" + current };\n            }\n          }\n        }\n      }\n    }\n  }\n\n  return { state: \"success\", data: response };\n}\n// Install script dataType\njQuery.ajaxSetup({\n  accepts: {\n    script: \"text/javascript, application/javascript, application/ecmascript, application/x-ecmascript\"\n  },\n  contents: {\n    script: /(?:java|ecma)script/\n  },\n  converters: {\n    \"text script\": function( text ) {\n      jQuery.globalEval( text );\n      return text;\n    }\n  }\n});\n\n// Handle cache's special case and crossDomain\njQuery.ajaxPrefilter( \"script\", function( s ) {\n  if ( s.cache === undefined ) {\n    s.cache = false;\n  }\n  if ( s.crossDomain ) {\n    s.type = \"GET\";\n  }\n});\n\n// Bind script tag hack transport\njQuery.ajaxTransport( \"script\", function( s ) {\n  // This transport only deals with cross domain requests\n  if ( s.crossDomain ) {\n    var script, callback;\n    return {\n      send: function( _, complete ) {\n        script = jQuery(\"<script>\").prop({\n          async: true,\n          charset: s.scriptCharset,\n          src: s.url\n        }).on(\n          \"load error\",\n          callback = function( evt ) {\n            script.remove();\n            callback = null;\n            if ( evt ) {\n              complete( evt.type === \"error\" ? 404 : 200, evt.type );\n            }\n          }\n        );\n        document.head.appendChild( script[ 0 ] );\n      },\n      abort: function() {\n        if ( callback ) {\n          callback();\n        }\n      }\n    };\n  }\n});\nvar oldCallbacks = [],\n  rjsonp = /(=)\\?(?=&|$)|\\?\\?/;\n\n// Default jsonp settings\njQuery.ajaxSetup({\n  jsonp: \"callback\",\n  jsonpCallback: function() {\n    var callback = oldCallbacks.pop() || ( jQuery.expando + \"_\" + ( ajax_nonce++ ) );\n    this[ callback ] = true;\n    return callback;\n  }\n});\n\n// Detect, normalize options and install callbacks for jsonp requests\njQuery.ajaxPrefilter( \"json jsonp\", function( s, originalSettings, jqXHR ) {\n\n  var callbackName, overwritten, responseContainer,\n    jsonProp = s.jsonp !== false && ( rjsonp.test( s.url ) ?\n      \"url\" :\n      typeof s.data === \"string\" && !( s.contentType || \"\" ).indexOf(\"application/x-www-form-urlencoded\") && rjsonp.test( s.data ) && \"data\"\n    );\n\n  // Handle iff the expected data type is \"jsonp\" or we have a parameter to set\n  if ( jsonProp || s.dataTypes[ 0 ] === \"jsonp\" ) {\n\n    // Get callback name, remembering preexisting value associated with it\n    callbackName = s.jsonpCallback = jQuery.isFunction( s.jsonpCallback ) ?\n      s.jsonpCallback() :\n      s.jsonpCallback;\n\n    // Insert callback into url or form data\n    if ( jsonProp ) {\n      s[ jsonProp ] = s[ jsonProp ].replace( rjsonp, \"$1\" + callbackName );\n    } else if ( s.jsonp !== false ) {\n      s.url += ( ajax_rquery.test( s.url ) ? \"&\" : \"?\" ) + s.jsonp + \"=\" + callbackName;\n    }\n\n    // Use data converter to retrieve json after script execution\n    s.converters[\"script json\"] = function() {\n      if ( !responseContainer ) {\n        jQuery.error( callbackName + \" was not called\" );\n      }\n      return responseContainer[ 0 ];\n    };\n\n    // force json dataType\n    s.dataTypes[ 0 ] = \"json\";\n\n    // Install callback\n    overwritten = window[ callbackName ];\n    window[ callbackName ] = function() {\n      responseContainer = arguments;\n    };\n\n    // Clean-up function (fires after converters)\n    jqXHR.always(function() {\n      // Restore preexisting value\n      window[ callbackName ] = overwritten;\n\n      // Save back as free\n      if ( s[ callbackName ] ) {\n        // make sure that re-using the options doesn't screw things around\n        s.jsonpCallback = originalSettings.jsonpCallback;\n\n        // save the callback name for future use\n        oldCallbacks.push( callbackName );\n      }\n\n      // Call if it was a function and we have a response\n      if ( responseContainer && jQuery.isFunction( overwritten ) ) {\n        overwritten( responseContainer[ 0 ] );\n      }\n\n      responseContainer = overwritten = undefined;\n    });\n\n    // Delegate to script\n    return \"script\";\n  }\n});\njQuery.ajaxSettings.xhr = function() {\n  try {\n    return new XMLHttpRequest();\n  } catch( e ) {}\n};\n\nvar xhrSupported = jQuery.ajaxSettings.xhr(),\n  xhrSuccessStatus = {\n    // file protocol always yields status code 0, assume 200\n    0: 200,\n    // Support: IE9\n    // #1450: sometimes IE returns 1223 when it should be 204\n    1223: 204\n  },\n  // Support: IE9\n  // We need to keep track of outbound xhr and abort them manually\n  // because IE is not smart enough to do it all by itself\n  xhrId = 0,\n  xhrCallbacks = {};\n\nif ( window.ActiveXObject ) {\n  jQuery( window ).on( \"unload\", function() {\n    for( var key in xhrCallbacks ) {\n      xhrCallbacks[ key ]();\n    }\n    xhrCallbacks = undefined;\n  });\n}\n\njQuery.support.cors = !!xhrSupported && ( \"withCredentials\" in xhrSupported );\njQuery.support.ajax = xhrSupported = !!xhrSupported;\n\njQuery.ajaxTransport(function( options ) {\n  var callback;\n  // Cross domain only allowed if supported through XMLHttpRequest\n  if ( jQuery.support.cors || xhrSupported && !options.crossDomain ) {\n    return {\n      send: function( headers, complete ) {\n        var i, id,\n          xhr = options.xhr();\n        xhr.open( options.type, options.url, options.async, options.username, options.password );\n        // Apply custom fields if provided\n        if ( options.xhrFields ) {\n          for ( i in options.xhrFields ) {\n            xhr[ i ] = options.xhrFields[ i ];\n          }\n        }\n        // Override mime type if needed\n        if ( options.mimeType && xhr.overrideMimeType ) {\n          xhr.overrideMimeType( options.mimeType );\n        }\n        // X-Requested-With header\n        // For cross-domain requests, seeing as conditions for a preflight are\n        // akin to a jigsaw puzzle, we simply never set it to be sure.\n        // (it can always be set on a per-request basis or even using ajaxSetup)\n        // For same-domain requests, won't change header if already provided.\n        if ( !options.crossDomain && !headers[\"X-Requested-With\"] ) {\n          headers[\"X-Requested-With\"] = \"XMLHttpRequest\";\n        }\n        // Set headers\n        for ( i in headers ) {\n          xhr.setRequestHeader( i, headers[ i ] );\n        }\n        // Callback\n        callback = function( type ) {\n          return function() {\n            if ( callback ) {\n              delete xhrCallbacks[ id ];\n              callback = xhr.onload = xhr.onerror = null;\n              if ( type === \"abort\" ) {\n                xhr.abort();\n              } else if ( type === \"error\" ) {\n                complete(\n                  // file protocol always yields status 0, assume 404\n                  xhr.status || 404,\n                  xhr.statusText\n                );\n              } else {\n                complete(\n                  xhrSuccessStatus[ xhr.status ] || xhr.status,\n                  xhr.statusText,\n                  // Support: IE9\n                  // #11426: When requesting binary data, IE9 will throw an exception\n                  // on any attempt to access responseText\n                  typeof xhr.responseText === \"string\" ? {\n                    text: xhr.responseText\n                  } : undefined,\n                  xhr.getAllResponseHeaders()\n                );\n              }\n            }\n          };\n        };\n        // Listen to events\n        xhr.onload = callback();\n        xhr.onerror = callback(\"error\");\n        // Create the abort callback\n        callback = xhrCallbacks[( id = xhrId++ )] = callback(\"abort\");\n        // Do send the request\n        // This may raise an exception which is actually\n        // handled in jQuery.ajax (so no try/catch here)\n        xhr.send( options.hasContent && options.data || null );\n      },\n      abort: function() {\n        if ( callback ) {\n          callback();\n        }\n      }\n    };\n  }\n});\nvar fxNow, timerId,\n  rfxtypes = /^(?:toggle|show|hide)$/,\n  rfxnum = new RegExp( \"^(?:([+-])=|)(\" + core_pnum + \")([a-z%]*)$\", \"i\" ),\n  rrun = /queueHooks$/,\n  animationPrefilters = [ defaultPrefilter ],\n  tweeners = {\n    \"*\": [function( prop, value ) {\n      var tween = this.createTween( prop, value ),\n        target = tween.cur(),\n        parts = rfxnum.exec( value ),\n        unit = parts && parts[ 3 ] || ( jQuery.cssNumber[ prop ] ? \"\" : \"px\" ),\n\n        // Starting value computation is required for potential unit mismatches\n        start = ( jQuery.cssNumber[ prop ] || unit !== \"px\" && +target ) &&\n          rfxnum.exec( jQuery.css( tween.elem, prop ) ),\n        scale = 1,\n        maxIterations = 20;\n\n      if ( start && start[ 3 ] !== unit ) {\n        // Trust units reported by jQuery.css\n        unit = unit || start[ 3 ];\n\n        // Make sure we update the tween properties later on\n        parts = parts || [];\n\n        // Iteratively approximate from a nonzero starting point\n        start = +target || 1;\n\n        do {\n          // If previous iteration zeroed out, double until we get *something*\n          // Use a string for doubling factor so we don't accidentally see scale as unchanged below\n          scale = scale || \".5\";\n\n          // Adjust and apply\n          start = start / scale;\n          jQuery.style( tween.elem, prop, start + unit );\n\n        // Update scale, tolerating zero or NaN from tween.cur()\n        // And breaking the loop if scale is unchanged or perfect, or if we've just had enough\n        } while ( scale !== (scale = tween.cur() / target) && scale !== 1 && --maxIterations );\n      }\n\n      // Update tween properties\n      if ( parts ) {\n        start = tween.start = +start || +target || 0;\n        tween.unit = unit;\n        // If a +=/-= token was provided, we're doing a relative animation\n        tween.end = parts[ 1 ] ?\n          start + ( parts[ 1 ] + 1 ) * parts[ 2 ] :\n          +parts[ 2 ];\n      }\n\n      return tween;\n    }]\n  };\n\n// Animations created synchronously will run synchronously\nfunction createFxNow() {\n  setTimeout(function() {\n    fxNow = undefined;\n  });\n  return ( fxNow = jQuery.now() );\n}\n\nfunction createTween( value, prop, animation ) {\n  var tween,\n    collection = ( tweeners[ prop ] || [] ).concat( tweeners[ \"*\" ] ),\n    index = 0,\n    length = collection.length;\n  for ( ; index < length; index++ ) {\n    if ( (tween = collection[ index ].call( animation, prop, value )) ) {\n\n      // we're done with this property\n      return tween;\n    }\n  }\n}\n\nfunction Animation( elem, properties, options ) {\n  var result,\n    stopped,\n    index = 0,\n    length = animationPrefilters.length,\n    deferred = jQuery.Deferred().always( function() {\n      // don't match elem in the :animated selector\n      delete tick.elem;\n    }),\n    tick = function() {\n      if ( stopped ) {\n        return false;\n      }\n      var currentTime = fxNow || createFxNow(),\n        remaining = Math.max( 0, animation.startTime + animation.duration - currentTime ),\n        // archaic crash bug won't allow us to use 1 - ( 0.5 || 0 ) (#12497)\n        temp = remaining / animation.duration || 0,\n        percent = 1 - temp,\n        index = 0,\n        length = animation.tweens.length;\n\n      for ( ; index < length ; index++ ) {\n        animation.tweens[ index ].run( percent );\n      }\n\n      deferred.notifyWith( elem, [ animation, percent, remaining ]);\n\n      if ( percent < 1 && length ) {\n        return remaining;\n      } else {\n        deferred.resolveWith( elem, [ animation ] );\n        return false;\n      }\n    },\n    animation = deferred.promise({\n      elem: elem,\n      props: jQuery.extend( {}, properties ),\n      opts: jQuery.extend( true, { specialEasing: {} }, options ),\n      originalProperties: properties,\n      originalOptions: options,\n      startTime: fxNow || createFxNow(),\n      duration: options.duration,\n      tweens: [],\n      createTween: function( prop, end ) {\n        var tween = jQuery.Tween( elem, animation.opts, prop, end,\n            animation.opts.specialEasing[ prop ] || animation.opts.easing );\n        animation.tweens.push( tween );\n        return tween;\n      },\n      stop: function( gotoEnd ) {\n        var index = 0,\n          // if we are going to the end, we want to run all the tweens\n          // otherwise we skip this part\n          length = gotoEnd ? animation.tweens.length : 0;\n        if ( stopped ) {\n          return this;\n        }\n        stopped = true;\n        for ( ; index < length ; index++ ) {\n          animation.tweens[ index ].run( 1 );\n        }\n\n        // resolve when we played the last frame\n        // otherwise, reject\n        if ( gotoEnd ) {\n          deferred.resolveWith( elem, [ animation, gotoEnd ] );\n        } else {\n          deferred.rejectWith( elem, [ animation, gotoEnd ] );\n        }\n        return this;\n      }\n    }),\n    props = animation.props;\n\n  propFilter( props, animation.opts.specialEasing );\n\n  for ( ; index < length ; index++ ) {\n    result = animationPrefilters[ index ].call( animation, elem, props, animation.opts );\n    if ( result ) {\n      return result;\n    }\n  }\n\n  jQuery.map( props, createTween, animation );\n\n  if ( jQuery.isFunction( animation.opts.start ) ) {\n    animation.opts.start.call( elem, animation );\n  }\n\n  jQuery.fx.timer(\n    jQuery.extend( tick, {\n      elem: elem,\n      anim: animation,\n      queue: animation.opts.queue\n    })\n  );\n\n  // attach callbacks from options\n  return animation.progress( animation.opts.progress )\n    .done( animation.opts.done, animation.opts.complete )\n    .fail( animation.opts.fail )\n    .always( animation.opts.always );\n}\n\nfunction propFilter( props, specialEasing ) {\n  var index, name, easing, value, hooks;\n\n  // camelCase, specialEasing and expand cssHook pass\n  for ( index in props ) {\n    name = jQuery.camelCase( index );\n    easing = specialEasing[ name ];\n    value = props[ index ];\n    if ( jQuery.isArray( value ) ) {\n      easing = value[ 1 ];\n      value = props[ index ] = value[ 0 ];\n    }\n\n    if ( index !== name ) {\n      props[ name ] = value;\n      delete props[ index ];\n    }\n\n    hooks = jQuery.cssHooks[ name ];\n    if ( hooks && \"expand\" in hooks ) {\n      value = hooks.expand( value );\n      delete props[ name ];\n\n      // not quite $.extend, this wont overwrite keys already present.\n      // also - reusing 'index' from above because we have the correct \"name\"\n      for ( index in value ) {\n        if ( !( index in props ) ) {\n          props[ index ] = value[ index ];\n          specialEasing[ index ] = easing;\n        }\n      }\n    } else {\n      specialEasing[ name ] = easing;\n    }\n  }\n}\n\njQuery.Animation = jQuery.extend( Animation, {\n\n  tweener: function( props, callback ) {\n    if ( jQuery.isFunction( props ) ) {\n      callback = props;\n      props = [ \"*\" ];\n    } else {\n      props = props.split(\" \");\n    }\n\n    var prop,\n      index = 0,\n      length = props.length;\n\n    for ( ; index < length ; index++ ) {\n      prop = props[ index ];\n      tweeners[ prop ] = tweeners[ prop ] || [];\n      tweeners[ prop ].unshift( callback );\n    }\n  },\n\n  prefilter: function( callback, prepend ) {\n    if ( prepend ) {\n      animationPrefilters.unshift( callback );\n    } else {\n      animationPrefilters.push( callback );\n    }\n  }\n});\n\nfunction defaultPrefilter( elem, props, opts ) {\n  /* jshint validthis: true */\n  var prop, value, toggle, tween, hooks, oldfire,\n    anim = this,\n    orig = {},\n    style = elem.style,\n    hidden = elem.nodeType && isHidden( elem ),\n    dataShow = data_priv.get( elem, \"fxshow\" );\n\n  // handle queue: false promises\n  if ( !opts.queue ) {\n    hooks = jQuery._queueHooks( elem, \"fx\" );\n    if ( hooks.unqueued == null ) {\n      hooks.unqueued = 0;\n      oldfire = hooks.empty.fire;\n      hooks.empty.fire = function() {\n        if ( !hooks.unqueued ) {\n          oldfire();\n        }\n      };\n    }\n    hooks.unqueued++;\n\n    anim.always(function() {\n      // doing this makes sure that the complete handler will be called\n      // before this completes\n      anim.always(function() {\n        hooks.unqueued--;\n        if ( !jQuery.queue( elem, \"fx\" ).length ) {\n          hooks.empty.fire();\n        }\n      });\n    });\n  }\n\n  // height/width overflow pass\n  if ( elem.nodeType === 1 && ( \"height\" in props || \"width\" in props ) ) {\n    // Make sure that nothing sneaks out\n    // Record all 3 overflow attributes because IE9-10 do not\n    // change the overflow attribute when overflowX and\n    // overflowY are set to the same value\n    opts.overflow = [ style.overflow, style.overflowX, style.overflowY ];\n\n    // Set display property to inline-block for height/width\n    // animations on inline elements that are having width/height animated\n    if ( jQuery.css( elem, \"display\" ) === \"inline\" &&\n        jQuery.css( elem, \"float\" ) === \"none\" ) {\n\n      style.display = \"inline-block\";\n    }\n  }\n\n  if ( opts.overflow ) {\n    style.overflow = \"hidden\";\n    anim.always(function() {\n      style.overflow = opts.overflow[ 0 ];\n      style.overflowX = opts.overflow[ 1 ];\n      style.overflowY = opts.overflow[ 2 ];\n    });\n  }\n\n\n  // show/hide pass\n  for ( prop in props ) {\n    value = props[ prop ];\n    if ( rfxtypes.exec( value ) ) {\n      delete props[ prop ];\n      toggle = toggle || value === \"toggle\";\n      if ( value === ( hidden ? \"hide\" : \"show\" ) ) {\n\n        // If there is dataShow left over from a stopped hide or show and we are going to proceed with show, we should pretend to be hidden\n        if ( value === \"show\" && dataShow && dataShow[ prop ] !== undefined ) {\n          hidden = true;\n        } else {\n          continue;\n        }\n      }\n      orig[ prop ] = dataShow && dataShow[ prop ] || jQuery.style( elem, prop );\n    }\n  }\n\n  if ( !jQuery.isEmptyObject( orig ) ) {\n    if ( dataShow ) {\n      if ( \"hidden\" in dataShow ) {\n        hidden = dataShow.hidden;\n      }\n    } else {\n      dataShow = data_priv.access( elem, \"fxshow\", {} );\n    }\n\n    // store state if its toggle - enables .stop().toggle() to \"reverse\"\n    if ( toggle ) {\n      dataShow.hidden = !hidden;\n    }\n    if ( hidden ) {\n      jQuery( elem ).show();\n    } else {\n      anim.done(function() {\n        jQuery( elem ).hide();\n      });\n    }\n    anim.done(function() {\n      var prop;\n\n      data_priv.remove( elem, \"fxshow\" );\n      for ( prop in orig ) {\n        jQuery.style( elem, prop, orig[ prop ] );\n      }\n    });\n    for ( prop in orig ) {\n      tween = createTween( hidden ? dataShow[ prop ] : 0, prop, anim );\n\n      if ( !( prop in dataShow ) ) {\n        dataShow[ prop ] = tween.start;\n        if ( hidden ) {\n          tween.end = tween.start;\n          tween.start = prop === \"width\" || prop === \"height\" ? 1 : 0;\n        }\n      }\n    }\n  }\n}\n\nfunction Tween( elem, options, prop, end, easing ) {\n  return new Tween.prototype.init( elem, options, prop, end, easing );\n}\njQuery.Tween = Tween;\n\nTween.prototype = {\n  constructor: Tween,\n  init: function( elem, options, prop, end, easing, unit ) {\n    this.elem = elem;\n    this.prop = prop;\n    this.easing = easing || \"swing\";\n    this.options = options;\n    this.start = this.now = this.cur();\n    this.end = end;\n    this.unit = unit || ( jQuery.cssNumber[ prop ] ? \"\" : \"px\" );\n  },\n  cur: function() {\n    var hooks = Tween.propHooks[ this.prop ];\n\n    return hooks && hooks.get ?\n      hooks.get( this ) :\n      Tween.propHooks._default.get( this );\n  },\n  run: function( percent ) {\n    var eased,\n      hooks = Tween.propHooks[ this.prop ];\n\n    if ( this.options.duration ) {\n      this.pos = eased = jQuery.easing[ this.easing ](\n        percent, this.options.duration * percent, 0, 1, this.options.duration\n      );\n    } else {\n      this.pos = eased = percent;\n    }\n    this.now = ( this.end - this.start ) * eased + this.start;\n\n    if ( this.options.step ) {\n      this.options.step.call( this.elem, this.now, this );\n    }\n\n    if ( hooks && hooks.set ) {\n      hooks.set( this );\n    } else {\n      Tween.propHooks._default.set( this );\n    }\n    return this;\n  }\n};\n\nTween.prototype.init.prototype = Tween.prototype;\n\nTween.propHooks = {\n  _default: {\n    get: function( tween ) {\n      var result;\n\n      if ( tween.elem[ tween.prop ] != null &&\n        (!tween.elem.style || tween.elem.style[ tween.prop ] == null) ) {\n        return tween.elem[ tween.prop ];\n      }\n\n      // passing an empty string as a 3rd parameter to .css will automatically\n      // attempt a parseFloat and fallback to a string if the parse fails\n      // so, simple values such as \"10px\" are parsed to Float.\n      // complex values such as \"rotate(1rad)\" are returned as is.\n      result = jQuery.css( tween.elem, tween.prop, \"\" );\n      // Empty strings, null, undefined and \"auto\" are converted to 0.\n      return !result || result === \"auto\" ? 0 : result;\n    },\n    set: function( tween ) {\n      // use step hook for back compat - use cssHook if its there - use .style if its\n      // available and use plain properties where available\n      if ( jQuery.fx.step[ tween.prop ] ) {\n        jQuery.fx.step[ tween.prop ]( tween );\n      } else if ( tween.elem.style && ( tween.elem.style[ jQuery.cssProps[ tween.prop ] ] != null || jQuery.cssHooks[ tween.prop ] ) ) {\n        jQuery.style( tween.elem, tween.prop, tween.now + tween.unit );\n      } else {\n        tween.elem[ tween.prop ] = tween.now;\n      }\n    }\n  }\n};\n\n// Support: IE9\n// Panic based approach to setting things on disconnected nodes\n\nTween.propHooks.scrollTop = Tween.propHooks.scrollLeft = {\n  set: function( tween ) {\n    if ( tween.elem.nodeType && tween.elem.parentNode ) {\n      tween.elem[ tween.prop ] = tween.now;\n    }\n  }\n};\n\njQuery.each([ \"toggle\", \"show\", \"hide\" ], function( i, name ) {\n  var cssFn = jQuery.fn[ name ];\n  jQuery.fn[ name ] = function( speed, easing, callback ) {\n    return speed == null || typeof speed === \"boolean\" ?\n      cssFn.apply( this, arguments ) :\n      this.animate( genFx( name, true ), speed, easing, callback );\n  };\n});\n\njQuery.fn.extend({\n  fadeTo: function( speed, to, easing, callback ) {\n\n    // show any hidden elements after setting opacity to 0\n    return this.filter( isHidden ).css( \"opacity\", 0 ).show()\n\n      // animate to the value specified\n      .end().animate({ opacity: to }, speed, easing, callback );\n  },\n  animate: function( prop, speed, easing, callback ) {\n    var empty = jQuery.isEmptyObject( prop ),\n      optall = jQuery.speed( speed, easing, callback ),\n      doAnimation = function() {\n        // Operate on a copy of prop so per-property easing won't be lost\n        var anim = Animation( this, jQuery.extend( {}, prop ), optall );\n\n        // Empty animations, or finishing resolves immediately\n        if ( empty || data_priv.get( this, \"finish\" ) ) {\n          anim.stop( true );\n        }\n      };\n      doAnimation.finish = doAnimation;\n\n    return empty || optall.queue === false ?\n      this.each( doAnimation ) :\n      this.queue( optall.queue, doAnimation );\n  },\n  stop: function( type, clearQueue, gotoEnd ) {\n    var stopQueue = function( hooks ) {\n      var stop = hooks.stop;\n      delete hooks.stop;\n      stop( gotoEnd );\n    };\n\n    if ( typeof type !== \"string\" ) {\n      gotoEnd = clearQueue;\n      clearQueue = type;\n      type = undefined;\n    }\n    if ( clearQueue && type !== false ) {\n      this.queue( type || \"fx\", [] );\n    }\n\n    return this.each(function() {\n      var dequeue = true,\n        index = type != null && type + \"queueHooks\",\n        timers = jQuery.timers,\n        data = data_priv.get( this );\n\n      if ( index ) {\n        if ( data[ index ] && data[ index ].stop ) {\n          stopQueue( data[ index ] );\n        }\n      } else {\n        for ( index in data ) {\n          if ( data[ index ] && data[ index ].stop && rrun.test( index ) ) {\n            stopQueue( data[ index ] );\n          }\n        }\n      }\n\n      for ( index = timers.length; index--; ) {\n        if ( timers[ index ].elem === this && (type == null || timers[ index ].queue === type) ) {\n          timers[ index ].anim.stop( gotoEnd );\n          dequeue = false;\n          timers.splice( index, 1 );\n        }\n      }\n\n      // start the next in the queue if the last step wasn't forced\n      // timers currently will call their complete callbacks, which will dequeue\n      // but only if they were gotoEnd\n      if ( dequeue || !gotoEnd ) {\n        jQuery.dequeue( this, type );\n      }\n    });\n  },\n  finish: function( type ) {\n    if ( type !== false ) {\n      type = type || \"fx\";\n    }\n    return this.each(function() {\n      var index,\n        data = data_priv.get( this ),\n        queue = data[ type + \"queue\" ],\n        hooks = data[ type + \"queueHooks\" ],\n        timers = jQuery.timers,\n        length = queue ? queue.length : 0;\n\n      // enable finishing flag on private data\n      data.finish = true;\n\n      // empty the queue first\n      jQuery.queue( this, type, [] );\n\n      if ( hooks && hooks.stop ) {\n        hooks.stop.call( this, true );\n      }\n\n      // look for any active animations, and finish them\n      for ( index = timers.length; index--; ) {\n        if ( timers[ index ].elem === this && timers[ index ].queue === type ) {\n          timers[ index ].anim.stop( true );\n          timers.splice( index, 1 );\n        }\n      }\n\n      // look for any animations in the old queue and finish them\n      for ( index = 0; index < length; index++ ) {\n        if ( queue[ index ] && queue[ index ].finish ) {\n          queue[ index ].finish.call( this );\n        }\n      }\n\n      // turn off finishing flag\n      delete data.finish;\n    });\n  }\n});\n\n// Generate parameters to create a standard animation\nfunction genFx( type, includeWidth ) {\n  var which,\n    attrs = { height: type },\n    i = 0;\n\n  // if we include width, step value is 1 to do all cssExpand values,\n  // if we don't include width, step value is 2 to skip over Left and Right\n  includeWidth = includeWidth? 1 : 0;\n  for( ; i < 4 ; i += 2 - includeWidth ) {\n    which = cssExpand[ i ];\n    attrs[ \"margin\" + which ] = attrs[ \"padding\" + which ] = type;\n  }\n\n  if ( includeWidth ) {\n    attrs.opacity = attrs.width = type;\n  }\n\n  return attrs;\n}\n\n// Generate shortcuts for custom animations\njQuery.each({\n  slideDown: genFx(\"show\"),\n  slideUp: genFx(\"hide\"),\n  slideToggle: genFx(\"toggle\"),\n  fadeIn: { opacity: \"show\" },\n  fadeOut: { opacity: \"hide\" },\n  fadeToggle: { opacity: \"toggle\" }\n}, function( name, props ) {\n  jQuery.fn[ name ] = function( speed, easing, callback ) {\n    return this.animate( props, speed, easing, callback );\n  };\n});\n\njQuery.speed = function( speed, easing, fn ) {\n  var opt = speed && typeof speed === \"object\" ? jQuery.extend( {}, speed ) : {\n    complete: fn || !fn && easing ||\n      jQuery.isFunction( speed ) && speed,\n    duration: speed,\n    easing: fn && easing || easing && !jQuery.isFunction( easing ) && easing\n  };\n\n  opt.duration = jQuery.fx.off ? 0 : typeof opt.duration === \"number\" ? opt.duration :\n    opt.duration in jQuery.fx.speeds ? jQuery.fx.speeds[ opt.duration ] : jQuery.fx.speeds._default;\n\n  // normalize opt.queue - true/undefined/null -> \"fx\"\n  if ( opt.queue == null || opt.queue === true ) {\n    opt.queue = \"fx\";\n  }\n\n  // Queueing\n  opt.old = opt.complete;\n\n  opt.complete = function() {\n    if ( jQuery.isFunction( opt.old ) ) {\n      opt.old.call( this );\n    }\n\n    if ( opt.queue ) {\n      jQuery.dequeue( this, opt.queue );\n    }\n  };\n\n  return opt;\n};\n\njQuery.easing = {\n  linear: function( p ) {\n    return p;\n  },\n  swing: function( p ) {\n    return 0.5 - Math.cos( p*Math.PI ) / 2;\n  }\n};\n\njQuery.timers = [];\njQuery.fx = Tween.prototype.init;\njQuery.fx.tick = function() {\n  var timer,\n    timers = jQuery.timers,\n    i = 0;\n\n  fxNow = jQuery.now();\n\n  for ( ; i < timers.length; i++ ) {\n    timer = timers[ i ];\n    // Checks the timer has not already been removed\n    if ( !timer() && timers[ i ] === timer ) {\n      timers.splice( i--, 1 );\n    }\n  }\n\n  if ( !timers.length ) {\n    jQuery.fx.stop();\n  }\n  fxNow = undefined;\n};\n\njQuery.fx.timer = function( timer ) {\n  if ( timer() && jQuery.timers.push( timer ) ) {\n    jQuery.fx.start();\n  }\n};\n\njQuery.fx.interval = 13;\n\njQuery.fx.start = function() {\n  if ( !timerId ) {\n    timerId = setInterval( jQuery.fx.tick, jQuery.fx.interval );\n  }\n};\n\njQuery.fx.stop = function() {\n  clearInterval( timerId );\n  timerId = null;\n};\n\njQuery.fx.speeds = {\n  slow: 600,\n  fast: 200,\n  // Default speed\n  _default: 400\n};\n\n// Back Compat <1.8 extension point\njQuery.fx.step = {};\n\nif ( jQuery.expr && jQuery.expr.filters ) {\n  jQuery.expr.filters.animated = function( elem ) {\n    return jQuery.grep(jQuery.timers, function( fn ) {\n      return elem === fn.elem;\n    }).length;\n  };\n}\njQuery.fn.offset = function( options ) {\n  if ( arguments.length ) {\n    return options === undefined ?\n      this :\n      this.each(function( i ) {\n        jQuery.offset.setOffset( this, options, i );\n      });\n  }\n\n  var docElem, win,\n    elem = this[ 0 ],\n    box = { top: 0, left: 0 },\n    doc = elem && elem.ownerDocument;\n\n  if ( !doc ) {\n    return;\n  }\n\n  docElem = doc.documentElement;\n\n  // Make sure it's not a disconnected DOM node\n  if ( !jQuery.contains( docElem, elem ) ) {\n    return box;\n  }\n\n  // If we don't have gBCR, just use 0,0 rather than error\n  // BlackBerry 5, iOS 3 (original iPhone)\n  if ( typeof elem.getBoundingClientRect !== core_strundefined ) {\n    box = elem.getBoundingClientRect();\n  }\n  win = getWindow( doc );\n  return {\n    top: box.top + win.pageYOffset - docElem.clientTop,\n    left: box.left + win.pageXOffset - docElem.clientLeft\n  };\n};\n\njQuery.offset = {\n\n  setOffset: function( elem, options, i ) {\n    var curPosition, curLeft, curCSSTop, curTop, curOffset, curCSSLeft, calculatePosition,\n      position = jQuery.css( elem, \"position\" ),\n      curElem = jQuery( elem ),\n      props = {};\n\n    // Set position first, in-case top/left are set even on static elem\n    if ( position === \"static\" ) {\n      elem.style.position = \"relative\";\n    }\n\n    curOffset = curElem.offset();\n    curCSSTop = jQuery.css( elem, \"top\" );\n    curCSSLeft = jQuery.css( elem, \"left\" );\n    calculatePosition = ( position === \"absolute\" || position === \"fixed\" ) && ( curCSSTop + curCSSLeft ).indexOf(\"auto\") > -1;\n\n    // Need to be able to calculate position if either top or left is auto and position is either absolute or fixed\n    if ( calculatePosition ) {\n      curPosition = curElem.position();\n      curTop = curPosition.top;\n      curLeft = curPosition.left;\n\n    } else {\n      curTop = parseFloat( curCSSTop ) || 0;\n      curLeft = parseFloat( curCSSLeft ) || 0;\n    }\n\n    if ( jQuery.isFunction( options ) ) {\n      options = options.call( elem, i, curOffset );\n    }\n\n    if ( options.top != null ) {\n      props.top = ( options.top - curOffset.top ) + curTop;\n    }\n    if ( options.left != null ) {\n      props.left = ( options.left - curOffset.left ) + curLeft;\n    }\n\n    if ( \"using\" in options ) {\n      options.using.call( elem, props );\n\n    } else {\n      curElem.css( props );\n    }\n  }\n};\n\n\njQuery.fn.extend({\n\n  position: function() {\n    if ( !this[ 0 ] ) {\n      return;\n    }\n\n    var offsetParent, offset,\n      elem = this[ 0 ],\n      parentOffset = { top: 0, left: 0 };\n\n    // Fixed elements are offset from window (parentOffset = {top:0, left: 0}, because it is it's only offset parent\n    if ( jQuery.css( elem, \"position\" ) === \"fixed\" ) {\n      // We assume that getBoundingClientRect is available when computed position is fixed\n      offset = elem.getBoundingClientRect();\n\n    } else {\n      // Get *real* offsetParent\n      offsetParent = this.offsetParent();\n\n      // Get correct offsets\n      offset = this.offset();\n      if ( !jQuery.nodeName( offsetParent[ 0 ], \"html\" ) ) {\n        parentOffset = offsetParent.offset();\n      }\n\n      // Add offsetParent borders\n      parentOffset.top += jQuery.css( offsetParent[ 0 ], \"borderTopWidth\", true );\n      parentOffset.left += jQuery.css( offsetParent[ 0 ], \"borderLeftWidth\", true );\n    }\n\n    // Subtract parent offsets and element margins\n    return {\n      top: offset.top - parentOffset.top - jQuery.css( elem, \"marginTop\", true ),\n      left: offset.left - parentOffset.left - jQuery.css( elem, \"marginLeft\", true )\n    };\n  },\n\n  offsetParent: function() {\n    return this.map(function() {\n      var offsetParent = this.offsetParent || docElem;\n\n      while ( offsetParent && ( !jQuery.nodeName( offsetParent, \"html\" ) && jQuery.css( offsetParent, \"position\") === \"static\" ) ) {\n        offsetParent = offsetParent.offsetParent;\n      }\n\n      return offsetParent || docElem;\n    });\n  }\n});\n\n\n// Create scrollLeft and scrollTop methods\njQuery.each( {scrollLeft: \"pageXOffset\", scrollTop: \"pageYOffset\"}, function( method, prop ) {\n  var top = \"pageYOffset\" === prop;\n\n  jQuery.fn[ method ] = function( val ) {\n    return jQuery.access( this, function( elem, method, val ) {\n      var win = getWindow( elem );\n\n      if ( val === undefined ) {\n        return win ? win[ prop ] : elem[ method ];\n      }\n\n      if ( win ) {\n        win.scrollTo(\n          !top ? val : window.pageXOffset,\n          top ? val : window.pageYOffset\n        );\n\n      } else {\n        elem[ method ] = val;\n      }\n    }, method, val, arguments.length, null );\n  };\n});\n\nfunction getWindow( elem ) {\n  return jQuery.isWindow( elem ) ? elem : elem.nodeType === 9 && elem.defaultView;\n}\n// Create innerHeight, innerWidth, height, width, outerHeight and outerWidth methods\njQuery.each( { Height: \"height\", Width: \"width\" }, function( name, type ) {\n  jQuery.each( { padding: \"inner\" + name, content: type, \"\": \"outer\" + name }, function( defaultExtra, funcName ) {\n    // margin is only for outerHeight, outerWidth\n    jQuery.fn[ funcName ] = function( margin, value ) {\n      var chainable = arguments.length && ( defaultExtra || typeof margin !== \"boolean\" ),\n        extra = defaultExtra || ( margin === true || value === true ? \"margin\" : \"border\" );\n\n      return jQuery.access( this, function( elem, type, value ) {\n        var doc;\n\n        if ( jQuery.isWindow( elem ) ) {\n          // As of 5/8/2012 this will yield incorrect results for Mobile Safari, but there\n          // isn't a whole lot we can do. See pull request at this URL for discussion:\n          // https://github.com/jquery/jquery/pull/764\n          return elem.document.documentElement[ \"client\" + name ];\n        }\n\n        // Get document width or height\n        if ( elem.nodeType === 9 ) {\n          doc = elem.documentElement;\n\n          // Either scroll[Width/Height] or offset[Width/Height] or client[Width/Height],\n          // whichever is greatest\n          return Math.max(\n            elem.body[ \"scroll\" + name ], doc[ \"scroll\" + name ],\n            elem.body[ \"offset\" + name ], doc[ \"offset\" + name ],\n            doc[ \"client\" + name ]\n          );\n        }\n\n        return value === undefined ?\n          // Get width or height on the element, requesting but not forcing parseFloat\n          jQuery.css( elem, type, extra ) :\n\n          // Set width or height on the element\n          jQuery.style( elem, type, value, extra );\n      }, type, chainable ? margin : undefined, chainable, null );\n    };\n  });\n});\n// Limit scope pollution from any deprecated API\n// (function() {\n\n// The number of elements contained in the matched element set\njQuery.fn.size = function() {\n  return this.length;\n};\n\njQuery.fn.andSelf = jQuery.fn.addBack;\n\n// })();\nif ( typeof module === \"object\" && module && typeof module.exports === \"object\" ) {\n  // Expose jQuery as module.exports in loaders that implement the Node\n  // module pattern (including browserify). Do not create the global, since\n  // the user will be storing it themselves locally, and globals are frowned\n  // upon in the Node module world.\n  module.exports = jQuery;\n} else {\n  // Register as a named AMD module, since jQuery can be concatenated with other\n  // files that may use define, but not via a proper concatenation script that\n  // understands anonymous AMD modules. A named AMD is safest and most robust\n  // way to register. Lowercase jquery is used because AMD module names are\n  // derived from file names, and jQuery is normally delivered in a lowercase\n  // file name. Do this after creating the global so that if an AMD module wants\n  // to call noConflict to hide this version of jQuery, it will work.\n  if ( typeof define === \"function\" && define.amd ) {\n    define( \"jquery\", [], function () { return jQuery; } );\n  }\n}\n\n// If there is a window object, that at least has a document property,\n// define jQuery and $ identifiers\nif ( typeof window === \"object\" && typeof window.document === \"object\" ) {\n  window.jQuery = window.$ = jQuery;\n}\n\n})( window );\n"
  },
  {
    "path": "examples/django_example/django_example/static/js/js.cookie.js",
    "content": "/*! js-cookie v2.0.3 | MIT */\n!function(a){if(\"function\"==typeof define&&define.amd)define(a);else if(\"object\"==typeof exports)module.exports=a();else{var b=window.Cookies,c=window.Cookies=a(window.jQuery);c.noConflict=function(){return window.Cookies=b,c}}}(function(){function a(){for(var a=0,b={};a<arguments.length;a++){var c=arguments[a];for(var d in c)b[d]=c[d]}return b}function b(c){function d(b,e,f){var g;if(arguments.length>1){if(f=a({path:\"/\"},d.defaults,f),\"number\"==typeof f.expires){var h=new Date;h.setMilliseconds(h.getMilliseconds()+864e5*f.expires),f.expires=h}try{g=JSON.stringify(e),/^[\\{\\[]/.test(g)&&(e=g)}catch(i){}return e=encodeURIComponent(String(e)),e=e.replace(/%(23|24|26|2B|3A|3C|3E|3D|2F|3F|40|5B|5D|5E|60|7B|7D|7C)/g,decodeURIComponent),b=encodeURIComponent(String(b)),b=b.replace(/%(23|24|26|2B|5E|60|7C)/g,decodeURIComponent),b=b.replace(/[\\(\\)]/g,escape),document.cookie=[b,\"=\",e,f.expires&&\"; expires=\"+f.expires.toUTCString(),f.path&&\"; path=\"+f.path,f.domain&&\"; domain=\"+f.domain,f.secure?\"; secure\":\"\"].join(\"\")}b||(g={});for(var j=document.cookie?document.cookie.split(\"; \"):[],k=/(%[0-9A-Z]{2})+/g,l=0;l<j.length;l++){var m=j[l].split(\"=\"),n=m[0].replace(k,decodeURIComponent),o=m.slice(1).join(\"=\");'\"'===o.charAt(0)&&(o=o.slice(1,-1));try{if(o=c&&c(o,n)||o.replace(k,decodeURIComponent),this.json)try{o=JSON.parse(o)}catch(i){}if(b===n){g=o;break}b||(g[n]=o)}catch(i){}}return g}return d.get=d.set=d,d.getJSON=function(){return d.apply({json:!0},[].slice.call(arguments))},d.defaults={},d.remove=function(b,c){d(b,\"\",a(c,{expires:-1}))},d.withConverter=b,d}return b()});"
  },
  {
    "path": "examples/django_example/django_example/templates/app.html",
    "content": "{% load static %}\n<!doctype html>\n<html>\n  <head>\n    <title>Django ChatterBot Example</title>\n    <link rel=\"shortcut icon\" href=\"{% static 'favicon.ico' %}\"/>\n    <link rel=\"stylesheet\" href=\"{% static 'css/bootstrap.css' %}\"/>\n    <link rel=\"stylesheet\" href=\"{% static 'css/custom.css' %}\"/>\n  </head>\n  <body>\n\n    {% include 'nav.html' %}\n\n    <div class=\"container mt-3 pt-3\">\n\n      <div class=\"jumbotron mt-3\">\n        <h1 class=\"jumbotron-heading text-center\">Django ChatterBot Example</h1>\n        <p class=\"lead text-center\">\n          This is a web app that allows you to talk to ChatterBot.\n        </p>\n\n        <hr class=\"my-2\">\n\n        <div class=\"row\">\n          <div class=\"col-lg-6 offset-lg-3\">\n            <div class=\"list-group chat-log js-chat-log\"></div>\n\n            <div class=\"input-group input-group-lg mt-2\">\n              <input type=\"text\" class=\"form-control js-text\" placeholder=\"Type something to begin...\"/>\n              <div class=\"input-group-append\">\n                <button class=\"btn btn-primary js-say\">Submit</button>\n              </span>\n            </div>\n            \n          </div>\n        </div>\n\n      </div>\n\n    </div>\n\n    <script src=\"{% static 'js/jquery.js' %}\"></script>\n    <script src=\"{% static 'js/js.cookie.js' %}\"></script>\n    <script src=\"{% static 'js/bootstrap.js' %}\"></script>\n    <script>\n      var chatterbotUrl = '{% url \"chatterbot\" %}';\n      var csrftoken = Cookies.get('csrftoken');\n\n      function csrfSafeMethod(method) {\n        // these HTTP methods do not require CSRF protection\n        return (/^(GET|HEAD|OPTIONS|TRACE)$/.test(method));\n      }\n\n      $.ajaxSetup({\n        beforeSend: function(xhr, settings) {\n          if (!csrfSafeMethod(settings.type) && !this.crossDomain) {\n            xhr.setRequestHeader(\"X-CSRFToken\", csrftoken);\n          }\n        }\n      });\n\n      var $chatlog = $('.js-chat-log');\n      var $input = $('.js-text');\n      var $sayButton = $('.js-say');\n\n      function createRow(text) {\n        var $row = $('<div class=\"list-group-item\"></div>');\n\n        $row.html(text);\n        $chatlog.append($row);\n\n        return $row;\n      }\n\n      function submitInput() {\n        var inputData = {\n          'text': $input.val()\n        }\n\n        // Display the user's input on the web page\n        createRow(inputData.text);\n\n        // Clear the input field\n        $input.val('');\n\n        // Add a loading indicator\n        var $loadingRow = createRow(\n          `\n          <div class=\"d-flex justify-content-center\">\n            <div class=\"spinner-border text-success\" role=\"status\">\n              <span class=\"sr-only\">Loading...</span>\n            </div>\n          </div>\n          `\n        );\n\n        var $submit = $.ajax({\n          type: 'POST',\n          url: chatterbotUrl,\n          data: JSON.stringify(inputData),\n          contentType: 'application/json'\n        });\n\n        $submit.done(function(statement) {\n\n            // RReplace the loading indicator with the bot's response\n            $loadingRow.html('');\n            $loadingRow.text(statement.text);\n\n            // Scroll to the bottom of the chat interface\n            $chatlog[0].scrollTop = $chatlog[0].scrollHeight;\n        });\n\n        $submit.fail(function(jqXHR, textStatus, errorThrown) {\n            // Handle the error\n\n            $loadingRow.html('An error occurred while processing your request. Check console for details.');\n            $loadingRow.addClass('list-group-item-danger');\n\n            console.error('Error:', textStatus, errorThrown);\n        });\n      }\n\n      $sayButton.click(function() {\n        submitInput();\n      });\n\n      $input.keydown(function(event) {\n        // Submit the input when the enter button is pressed\n        if (event.keyCode == 13) {\n          submitInput();\n        }\n      });\n    </script>\n  </body>\n</html>\n"
  },
  {
    "path": "examples/django_example/django_example/templates/nav.html",
    "content": "{% load static %}\n\n<nav class=\"navbar navbar-expand-lg navbar-dark bg-dark\">\n\n  <a class=\"navbar-brand\" href=\"{% url 'main' %}\">\n    <img src=\"{% static 'img/chatterbot.png' %}\" width=\"30\" height=\"30\" class=\"d-inline-block align-top\" alt=\"ChatterBot\">\n    ChatterBot\n  </a>\n  <button class=\"navbar-toggler\" type=\"button\" data-toggle=\"collapse\" data-target=\"#navbarNav\" aria-controls=\"navbarNav\" aria-expanded=\"false\" aria-label=\"Toggle navigation\">\n    <span class=\"navbar-toggler-icon\"></span>\n  </button>\n\n  <div class=\"collapse navbar-collapse\" id=\"navbarNav\">\n    <ul class=\"navbar-nav\">\n      <li class=\"nav-item\">\n        <a class=\"nav-link\" href=\"https://docs.chatterbot.us\">Documentation</a>\n      </li>\n      <li class=\"nav-item\">\n        <a class=\"nav-link\" href=\"https://github.com/gunthercox/ChatterBot\">GitHub</a>\n      </li>\n    </ul>\n\n    <ul class=\"navbar-nav ml-auto\">\n      <li class=\"nav-item\">\n        <a class=\"nav-link\" href=\"{% url 'chatterbot' %}\">API</a>\n      </li>\n      <li class=\"nav-item\">\n        <a class=\"nav-link\" href=\"{% url 'admin:index' %}\">Admin</a>\n      </li>\n    </ul>\n  </div>\n</nav>"
  },
  {
    "path": "examples/django_example/django_example/tests/__init__.py",
    "content": ""
  },
  {
    "path": "examples/django_example/django_example/tests/test_api.py",
    "content": "import json\nfrom django.test import TestCase\nfrom django.urls import reverse\n\n\nclass ApiTestCase(TestCase):\n\n    def setUp(self):\n        super().setUp()\n        self.api_url = reverse('chatterbot')\n\n    def test_invalid_text(self):\n        response = self.client.post(\n            self.api_url,\n            data=json.dumps({\n                'type': 'classmethod'\n            }),\n            content_type='application/json',\n            format='json'\n        )\n\n        self.assertEqual(response.status_code, 400)\n        self.assertIn('text', response.json())\n        self.assertEqual(['The attribute \"text\" is required.'], response.json()['text'])\n\n    def test_post(self):\n        \"\"\"\n        Test that a response is returned.\n        \"\"\"\n        response = self.client.post(\n            self.api_url,\n            data=json.dumps({\n                'text': 'How are you?'\n            }),\n            content_type='application/json',\n            format='json'\n        )\n\n        self.assertEqual(response.status_code, 200)\n        self.assertIn('text', response.json())\n        self.assertGreater(len(response.json()['text']), 1)\n        self.assertIn('in_response_to', response.json())\n\n    def test_post_unicode(self):\n        \"\"\"\n        Test that a response is returned.\n        \"\"\"\n        response = self.client.post(\n            self.api_url,\n            data=json.dumps({\n                'text': u'سلام'\n            }),\n            content_type='application/json',\n            format='json'\n        )\n\n        self.assertEqual(response.status_code, 200)\n        self.assertIn('text', response.json())\n        self.assertGreater(len(response.json()['text']), 1)\n        self.assertIn('in_response_to', response.json())\n\n    def test_escaped_unicode_post(self):\n        \"\"\"\n        Test that unicode reponce\n        \"\"\"\n        response = self.client.post(\n            self.api_url,\n            data=json.dumps({\n                'text': '\\u2013'\n            }),\n            content_type='application/json',\n            format=json\n        )\n\n        self.assertEqual(response.status_code, 200)\n        self.assertIn('text', response.json())\n        self.assertIn('in_response_to', response.json())\n\n    def test_post_tags(self):\n        post_data = {\n            'text': 'Good morning.',\n            'tags': [\n                'user:jen@example.com'\n            ]\n        }\n        response = self.client.post(\n            self.api_url,\n            data=json.dumps(post_data),\n            content_type='application/json',\n            format='json'\n        )\n\n        self.assertEqual(response.status_code, 200)\n        self.assertIn('text', response.json())\n        self.assertIn('in_response_to', response.json())\n        self.assertIn('tags', response.json())\n        self.assertEqual(response.json()['tags'], ['user:jen@example.com'])\n\n    def test_get(self):\n        response = self.client.get(self.api_url)\n\n        self.assertEqual(response.status_code, 200)\n\n    def test_patch(self):\n        response = self.client.patch(self.api_url)\n\n        self.assertEqual(response.status_code, 405)\n\n    def test_put(self):\n        response = self.client.put(self.api_url)\n\n        self.assertEqual(response.status_code, 405)\n\n    def test_delete(self):\n        response = self.client.delete(self.api_url)\n\n        self.assertEqual(response.status_code, 405)\n"
  },
  {
    "path": "examples/django_example/django_example/tests/test_example.py",
    "content": "import json\nfrom django.test import TestCase\nfrom django.urls import reverse\n\n\nclass ViewTestCase(TestCase):\n\n    def setUp(self):\n        super().setUp()\n        self.url = reverse('main')\n\n    def test_get_main_page(self):\n        \"\"\"\n        Test that the main page can be loaded.\n        \"\"\"\n        response = self.client.get(self.url)\n        self.assertEqual(response.status_code, 200)\n\n\nclass ApiTestCase(TestCase):\n    \"\"\"\n    Tests to make sure that the ChatterBot app is\n    properly working with the Django example app.\n    \"\"\"\n\n    def setUp(self):\n        super().setUp()\n        self.api_url = reverse('chatterbot')\n\n    def test_post(self):\n        \"\"\"\n        Test that a response is returned.\n        \"\"\"\n        data = {\n            'text': 'How are you?'\n        }\n        response = self.client.post(\n            self.api_url,\n            data=json.dumps(data),\n            content_type='application/json',\n            format='json'\n        )\n\n        self.assertEqual(response.status_code, 200)\n        self.assertIn('text', response.json())\n        self.assertIn('in_response_to', response.json())\n\n    def test_post_tags(self):\n        post_data = {\n            'text': 'Good morning.',\n            'tags': [\n                'user:jen@example.com'\n            ]\n        }\n        response = self.client.post(\n            self.api_url,\n            data=json.dumps(post_data),\n            content_type='application/json',\n            format='json'\n        )\n\n        self.assertEqual(response.status_code, 200)\n        self.assertIn('text', response.json())\n        self.assertIn('in_response_to', response.json())\n        self.assertIn('tags', response.json())\n        self.assertEqual(response.json()['tags'], ['user:jen@example.com'])\n\n\nclass ApiIntegrationTestCase(TestCase):\n    \"\"\"\n    Test to make sure the ChatterBot API view works\n    properly with the example Django app.\n    \"\"\"\n\n    def setUp(self):\n        super().setUp()\n        self.api_url = reverse('chatterbot')\n\n    def test_get(self):\n        response = self.client.get(self.api_url)\n\n        self.assertIn('name', response.json())\n"
  },
  {
    "path": "examples/django_example/django_example/urls.py",
    "content": "\"\"\"\nURL configuration for django_example project.\n\"\"\"\nfrom django.contrib import admin\nfrom django.urls import path\nfrom django_example.views import ChatterBotAppView, ChatterBotApiView\n\n\nurlpatterns = [\n    path('', ChatterBotAppView.as_view(), name='main'),\n    path('api/chatterbot/', ChatterBotApiView.as_view(), name='chatterbot'),\n    path('admin/', admin.site.urls, name='admin'),\n]\n"
  },
  {
    "path": "examples/django_example/django_example/views.py",
    "content": "import json\nfrom django.views.generic.base import TemplateView\nfrom django.views.generic import View\nfrom django.http import JsonResponse\nfrom django.conf import settings\n\nfrom chatterbot import ChatBot\n\n\nclass ChatterBotAppView(TemplateView):\n    template_name = 'app.html'\n\n\nclass ChatterBotApiView(View):\n    \"\"\"\n    Provide an API endpoint to interact with ChatterBot.\n    \"\"\"\n\n    chatterbot = ChatBot(**settings.CHATTERBOT)\n\n    def post(self, request, *args, **kwargs):\n        \"\"\"\n        Return a response to the statement in the posted data.\n\n        * The JSON data should contain a 'text' attribute.\n        \"\"\"\n        input_data = json.loads(request.body.decode('utf-8'))\n\n        if 'text' not in input_data:\n            return JsonResponse({\n                'text': [\n                    'The attribute \"text\" is required.'\n                ]\n            }, status=400)\n\n        response = self.chatterbot.get_response(**input_data)\n\n        response_data = response.serialize()\n\n        return JsonResponse(response_data, status=200)\n\n    def get(self, request, *args, **kwargs):\n        \"\"\"\n        Return data corresponding to the current conversation.\n        \"\"\"\n        return JsonResponse({\n            'name': self.chatterbot.name\n        })\n"
  },
  {
    "path": "examples/django_example/django_example/wsgi.py",
    "content": "\"\"\"\nWSGI config for django_example project.\n\nIt exposes the WSGI callable as a module-level variable named ``application``.\n\nFor more information on this file, see\nhttps://docs.djangoproject.com/en/4.2/howto/deployment/wsgi/\n\"\"\"\n\nimport os\n\nfrom django.core.wsgi import get_wsgi_application\n\nos.environ.setdefault('DJANGO_SETTINGS_MODULE', 'django_example.settings')\n\napplication = get_wsgi_application()\n"
  },
  {
    "path": "examples/django_example/manage.py",
    "content": "#!/usr/bin/env python\n\"\"\"Django's command-line utility for administrative tasks.\"\"\"\nimport os\nimport sys\n\n\ndef main():\n    \"\"\"Run administrative tasks.\"\"\"\n    os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'django_example.settings')\n    try:\n        from django.core.management import execute_from_command_line\n    except ImportError as exc:\n        raise ImportError(\n            \"Couldn't import Django. Are you sure it's installed and \"\n            \"available on your PYTHONPATH environment variable? Did you \"\n            \"forget to activate a virtual environment?\"\n        ) from exc\n    execute_from_command_line(sys.argv)\n\n\nif __name__ == '__main__':\n    main()\n"
  },
  {
    "path": "examples/django_example/requirements.txt",
    "content": "django>=4.2,<4.3\nchatterbot>=1.2,<2.0\n"
  },
  {
    "path": "examples/export_example.py",
    "content": "from chatterbot import ChatBot\nfrom chatterbot.trainers import ChatterBotCorpusTrainer\n\n'''\nThis is an example showing how to create an export file from\nan existing chat bot that can then be used to train other bots.\n'''\n\nchatbot = ChatBot('Export Example Bot')\n\n# First, lets train our bot with some data\ntrainer = ChatterBotCorpusTrainer(chatbot)\n\ntrainer.train('chatterbot.corpus.english')\n\n# Now we can export the data to a file\ntrainer.export_for_training('./my_export.json')\n"
  },
  {
    "path": "examples/learning_feedback_example.py",
    "content": "from chatterbot import ChatBot\nfrom chatterbot.conversation import Statement\n\n\"\"\"\nThis example shows how to create a chat bot that\nwill learn responses based on an additional feedback\nelement from the user.\n\"\"\"\n\n# Uncomment the following line to enable verbose logging\n# import logging\n# logging.basicConfig(level=logging.INFO)\n\n# Create a new instance of a ChatBot\nbot = ChatBot(\n    'Feedback Learning Bot',\n    storage_adapter='chatterbot.storage.SQLStorageAdapter'\n)\n\n\ndef get_feedback():\n\n    text = input()\n\n    if 'yes' in text.lower():\n        return True\n    elif 'no' in text.lower():\n        return False\n    else:\n        print('Please type either \"Yes\" or \"No\"')\n        return get_feedback()\n\n\nprint('Type something to begin...')\n\n# The following loop will execute each time the user enters input\nwhile True:\n    try:\n        input_statement = Statement(text=input())\n        response = bot.generate_response(\n            input_statement\n        )\n\n        print('\\n Is \"{}\" a coherent response to \"{}\"? \\n'.format(\n            response.text,\n            input_statement.text\n        ))\n        if get_feedback() is False:\n            print('please input the correct one')\n            correct_response = Statement(text=input())\n            bot.learn_response(correct_response, input_statement)\n            print('Responses added to bot!')\n\n    # Press ctrl-c or ctrl-d on the keyboard to exit\n    except (KeyboardInterrupt, EOFError, SystemExit):\n        break\n"
  },
  {
    "path": "examples/math_and_time.py",
    "content": "from chatterbot import ChatBot\n\n\nbot = ChatBot(\n    'Math & Time Bot',\n    logic_adapters=[\n        'chatterbot.logic.MathematicalEvaluation',\n        'chatterbot.logic.TimeLogicAdapter'\n    ]\n)\n\n# Print an example of getting one math based response\nresponse = bot.get_response('What is 4 + 9?')\nprint(response)\n\n# Print an example of getting one time based response\nresponse = bot.get_response('What time is it?')\nprint(response)\n"
  },
  {
    "path": "examples/memory_sql_example.py",
    "content": "from chatterbot import ChatBot\n\n# Uncomment the following lines to enable verbose logging\n# import logging\n# logging.basicConfig(level=logging.INFO)\n\n# Create a new instance of a ChatBot\nbot = ChatBot(\n    'SQLMemoryTerminal',\n    storage_adapter='chatterbot.storage.SQLStorageAdapter',\n    database_uri=None,\n    logic_adapters=[\n        'chatterbot.logic.MathematicalEvaluation',\n        'chatterbot.logic.TimeLogicAdapter',\n        'chatterbot.logic.BestMatch'\n    ]\n)\n\n# Get a few responses from the bot\n\nbot.get_response('What time is it?')\n\nbot.get_response('What is 7 plus 7?')\n"
  },
  {
    "path": "examples/ollama_example.py",
    "content": "\"\"\"\nEXPERIMENTAL: See https://docs.chatterbot.us/large-language-models/ for more information.\n\nExample of using the Ollama API with the Ollama Python client.\n\nThis example shows how to integrate Ollama models into ChatterBot's consensus\nvoting system and optionally enable tool calling for specialized tasks.\n\"\"\"\nfrom chatterbot import ChatBot\nimport uuid\n\n# Uncomment the following lines to enable verbose logging\n# import logging\n# logging.basicConfig(level=logging.INFO)\n\n\n# Create a new instance of a ChatBot\nbot = ChatBot(\n    'Ollama Example Bot',\n    logic_adapters=[\n        {\n            'import_path': 'chatterbot.logic.OllamaLogicAdapter',\n            'model': 'llama3.1',\n            'host': 'http://localhost:11434',\n            # Optionally enable tools:\n            'logic_adapters_as_tools': [\n                'chatterbot.logic.MathematicalEvaluation',\n                'chatterbot.logic.TimeLogicAdapter',\n            ]\n        }\n    ]\n)\n\nprint('Type something to begin...')\n\n# Generate a conversation ID so the LLM adapter can retrieve\n# previous messages and maintain context across turns.\nconversation_id = uuid.uuid4().hex\n\n# The following loop will execute each time the user enters input\nwhile True:\n    try:\n        user_input = input()\n\n        bot_response = bot.get_response(user_input, conversation=conversation_id)\n        print(bot_response)\n\n    # Press ctrl-c or ctrl-d on the keyboard to exit\n    except (KeyboardInterrupt, EOFError, SystemExit):\n        break\n"
  },
  {
    "path": "examples/openai_example.py",
    "content": "\"\"\"\nEXPERIMENTAL: See https://docs.chatterbot.us/large-language-models/ for more information.\n\nExample of using the OpenAI API with the OpenAI Python client.\n\nRequires OPENAI_API_KEY environment variable to be set.\n\nThis example shows how to integrate OpenAI models into ChatterBot's consensus\nvoting system and enable tool calling for specialized tasks.\n\"\"\"\nfrom chatterbot import ChatBot\nfrom dotenv import load_dotenv\nimport uuid\n\n# Load the OPENAI_API_KEY from the .env file\nload_dotenv('../.env')\n\n# Create a new instance of a ChatBot\nbot = ChatBot(\n    'OpenAI Example Bot',\n    logic_adapters=[\n        {\n            'import_path': 'chatterbot.logic.OpenAILogicAdapter',\n            'model': 'gpt-4o-mini',\n            # Enable tools for math, time, and unit conversion\n            'logic_adapters_as_tools': [\n                'chatterbot.logic.MathematicalEvaluation',\n                'chatterbot.logic.TimeLogicAdapter',\n                'chatterbot.logic.UnitConversion'\n            ]\n        }\n    ]\n)\n\nprint('Type something to begin...')\n\n# Generate a conversation ID so the LLM adapter can retrieve\n# previous messages and maintain context across turns.\nconversation_id = uuid.uuid4().hex\n\n# The following loop will execute each time the user enters input\nwhile True:\n    try:\n        user_input = input()\n\n        bot_response = bot.get_response(user_input, conversation=conversation_id)\n        print(bot_response)\n\n    # Press ctrl-c or ctrl-d on the keyboard to exit\n    except (KeyboardInterrupt, EOFError, SystemExit):\n        break\n"
  },
  {
    "path": "examples/specific_response_example.py",
    "content": "from chatterbot import ChatBot\n\n\n# Create a new instance of a ChatBot\nbot = ChatBot(\n    'Exact Response Example Bot',\n    storage_adapter='chatterbot.storage.SQLStorageAdapter',\n    logic_adapters=[\n        {\n            'import_path': 'chatterbot.logic.BestMatch'\n        },\n        {\n            'import_path': 'chatterbot.logic.SpecificResponseAdapter',\n            'input_text': 'Help me!',\n            'output_text': 'Ok, here is a link: https://docs.chatterbot.us'\n        }\n    ]\n)\n\n# Get a response given the specific input\nresponse = bot.get_response('Help me!')\nprint(response)\n"
  },
  {
    "path": "examples/tagged_dataset_example.py",
    "content": "from chatterbot import ChatBot\nfrom chatterbot.conversation import Statement\n\n\nchatbot = ChatBot(\n    'Example Bot',\n    # This database will be a temporary in-memory database\n    database_uri=None\n)\n\nlabel_a_statements = [\n    Statement(text='Hello', tags=['label_a']),\n    Statement(text='Hi', tags=['label_a']),\n    Statement(text='How are you?', tags=['label_a'])\n]\n\nlabel_b_statements = [\n    Statement(text='I like dogs.', tags=['label_b']),\n    Statement(text='I like cats.', tags=['label_b']),\n    Statement(text='I like animals.', tags=['label_b'])\n]\n\nall_statements = [\n    *label_a_statements,\n    *label_b_statements\n]\n\n# Populate search text since we're adding statements directly to the database\nfor statement in all_statements:\n    statement.search_text = chatbot.tagger.get_text_index_string(statement.text)\n\nchatbot.storage.create_many(all_statements)\n\n# Return a response from \"label_a_statements\"\nresponse_from_label_a = chatbot.get_response(\n    'How are you?',\n    additional_response_selection_parameters={\n        'tags': ['label_a']\n    }\n)\n\n# Return a response from \"label_b_statements\"\nresponse_from_label_b = chatbot.get_response(\n    'How are you?',\n    additional_response_selection_parameters={\n        'tags': ['label_b']\n    }\n)\n\nprint('Response from label_a collection:', response_from_label_a.text)\nprint('Response from label_b collection:', response_from_label_b.text)\n"
  },
  {
    "path": "examples/terminal_example.py",
    "content": "from chatterbot import ChatBot\n\n\n# Uncomment the following lines to enable verbose logging\n# import logging\n# logging.basicConfig(level=logging.INFO)\n\n# NOTE: The order of logic adapters is important\n# because the first logic adapter takes precedence\n# if a good response cannot be determined.\n\n# Create a new instance of a ChatBot\nbot = ChatBot(\n    'Terminal',\n    storage_adapter='chatterbot.storage.SQLStorageAdapter',\n    logic_adapters=[\n        'chatterbot.logic.BestMatch',\n        'chatterbot.logic.TimeLogicAdapter',\n        'chatterbot.logic.MathematicalEvaluation'\n    ],\n    database_uri='sqlite:///database.sqlite3'\n)\n\nprint('Type something to begin...')\n\n# The following loop will execute each time the user enters input\nwhile True:\n    try:\n        user_input = input()\n\n        bot_response = bot.get_response(user_input)\n\n        print(bot_response)\n\n    # Press ctrl-c or ctrl-d on the keyboard to exit\n    except (KeyboardInterrupt, EOFError, SystemExit):\n        break\n"
  },
  {
    "path": "examples/terminal_mongo_example.py",
    "content": "from chatterbot import ChatBot\n\n# Uncomment the following lines to enable verbose logging\n# import logging\n# logging.basicConfig(level=logging.INFO)\n\n# Create a new ChatBot instance\nbot = ChatBot(\n    'Terminal',\n    storage_adapter='chatterbot.storage.MongoDatabaseAdapter',\n    logic_adapters=[\n        'chatterbot.logic.BestMatch'\n    ],\n    database_uri='mongodb://localhost:27017/chatterbot-database'\n)\n\nprint('Type something to begin...')\n\nwhile True:\n    try:\n        user_input = input()\n\n        bot_response = bot.get_response(user_input)\n\n        print(bot_response)\n\n    # Press ctrl-c or ctrl-d on the keyboard to exit\n    except (KeyboardInterrupt, EOFError, SystemExit):\n        break\n"
  },
  {
    "path": "examples/tkinter_gui.py",
    "content": "from chatterbot import ChatBot\nimport tkinter as tk\ntry:\n    import ttk as ttk\n    import ScrolledText\nexcept ImportError:\n    import tkinter.ttk as ttk\n    import tkinter.scrolledtext as ScrolledText\nimport time\n\n\nclass TkinterGUIExample(tk.Tk):\n\n    def __init__(self, *args, **kwargs):\n        \"\"\"\n        Create & set window variables.\n        \"\"\"\n        tk.Tk.__init__(self, *args, **kwargs)\n\n        self.chatbot = ChatBot(\n            \"GUI Bot\",\n            storage_adapter=\"chatterbot.storage.SQLStorageAdapter\",\n            logic_adapters=[\n                \"chatterbot.logic.BestMatch\"\n            ],\n            database_uri=\"sqlite:///database.sqlite3\"\n        )\n\n        self.title(\"Chatterbot\")\n\n        self.initialize()\n\n    def initialize(self):\n        \"\"\"\n        Set window layout.\n        \"\"\"\n        self.grid()\n\n        self.respond = ttk.Button(self, text='Get Response', command=self.get_response)\n        self.respond.grid(column=0, row=0, sticky='nesw', padx=3, pady=3)\n\n        self.usr_input = ttk.Entry(self, state='normal')\n        self.usr_input.grid(column=1, row=0, sticky='nesw', padx=3, pady=3)\n\n        self.conversation_lbl = ttk.Label(self, anchor=tk.E, text='Conversation:')\n        self.conversation_lbl.grid(column=0, row=1, sticky='nesw', padx=3, pady=3)\n\n        self.conversation = ScrolledText.ScrolledText(self, state='disabled')\n        self.conversation.grid(column=0, row=2, columnspan=2, sticky='nesw', padx=3, pady=3)\n\n    def get_response(self):\n        \"\"\"\n        Get a response from the chatbot and display it.\n        \"\"\"\n        user_input = self.usr_input.get()\n        self.usr_input.delete(0, tk.END)\n\n        response = self.chatbot.get_response(user_input)\n\n        self.conversation['state'] = 'normal'\n        self.conversation.insert(\n            tk.END, \"Human: \" + user_input + \"\\n\" + \"ChatBot: \" + str(response.text) + \"\\n\"\n        )\n        self.conversation['state'] = 'disabled'\n\n        time.sleep(0.5)\n\n\ngui_example = TkinterGUIExample()\ngui_example.mainloop()\n"
  },
  {
    "path": "examples/training_example_chatterbot_corpus.py",
    "content": "from chatterbot import ChatBot\nfrom chatterbot.trainers import ChatterBotCorpusTrainer\nimport logging\n\n\n'''\nThis is an example showing how to train a chat bot using the\nChatterBot Corpus of conversation dialog.\n'''\n\n# Enable info level logging\nlogging.basicConfig(level=logging.INFO)\n\nchatbot = ChatBot('Example Bot')\n\n# Start by training our bot with the ChatterBot corpus data\ntrainer = ChatterBotCorpusTrainer(chatbot)\n\ntrainer.train(\n    'chatterbot.corpus.english'\n)\n\n# Now let's get a response to a greeting\nresponse = chatbot.get_response('How are you doing today?')\nprint(response)\n"
  },
  {
    "path": "examples/training_example_list_data.py",
    "content": "from chatterbot import ChatBot\nfrom chatterbot.trainers import ListTrainer\n\n\n'''\nThis is an example showing how to train a chat bot using the\nChatterBot ListTrainer.\n'''\n\nchatbot = ChatBot('Example Bot')\n\n# Start by training our bot with the ChatterBot corpus data\ntrainer = ListTrainer(chatbot)\n\ntrainer.train([\n    'Hello, how are you?',\n    'I am doing well.',\n    'That is good to hear.',\n    'Thank you'\n])\n\n# You can train with a second list of data to add response variations\n\ntrainer.train([\n    'Hello, how are you?',\n    'I am great.',\n    'That is awesome.',\n    'Thanks'\n])\n\n# Now let's get a response to a greeting\nresponse = chatbot.get_response('How are you doing today?')\nprint(response)\n"
  },
  {
    "path": "examples/training_example_ubuntu_corpus.py",
    "content": "\"\"\"\nThis example shows how to train a chat bot using the\nUbuntu Corpus of conversation dialog.\n\"\"\"\nimport logging\nfrom chatterbot import ChatBot\nfrom chatterbot.trainers import UbuntuCorpusTrainer\n\n# Enable info level logging\nlogging.basicConfig(level=logging.INFO)\n\nchatbot = ChatBot('Example Bot')\n\ntrainer = UbuntuCorpusTrainer(chatbot)\n\n# Start by training our bot with the Ubuntu corpus data\ntrainer.train(\n    'http://cs.mcgill.ca/~jpineau/datasets/ubuntu-corpus-1.0/ubuntu_dialogs.tgz',\n    limit=100\n)\n\n# Now let's get a response to a greeting\nresponse = chatbot.get_response('How are you doing today?')\nprint(response)\n"
  },
  {
    "path": "graphics/README.md",
    "content": "# ChatterBot Graphics\n\nConcept art and imagery for ChatterBot was designed by [Griffin Cox](https://github.com/griffincx).\n\nFiles using a `.xcf` format can be viewed and edited using [GIMP](https://www.gimp.org/).\n"
  },
  {
    "path": "pyproject.toml",
    "content": "[build-system]\nrequires = [\"setuptools\"]\nbuild-backend = \"setuptools.build_meta\"\n\n[tool.setuptools]\npackages=[\n    \"chatterbot\",\n    \"chatterbot.storage\",\n    \"chatterbot.logic\",\n    \"chatterbot.ext\",\n    \"chatterbot.ext.sqlalchemy_app\",\n    \"chatterbot.ext.django_chatterbot\",\n    \"chatterbot.ext.django_chatterbot.migrations\",\n]\n\n[tool.setuptools.dynamic]\nversion = {attr = \"chatterbot.__version__\"}\n\n[project]\nname = \"ChatterBot\"\nrequires-python = \">=3.9,<3.14\"\nurls = { Documentation = \"https://docs.chatterbot.us\", Repository = \"https://github.com/gunthercox/ChatterBot\", Changelog = \"https://github.com/gunthercox/ChatterBot/releases\" }\ndescription = \"ChatterBot is a machine learning, conversational dialog engine\"\nauthors = [\n  {name = \"Gunther Cox\"},\n]\nlicense = \"BSD-3-Clause\"\nreadme = \"README.md\"\ndynamic = [\"version\"]\nkeywords = [\n    \"ChatterBot\",\n    \"chatbot\",\n    \"chat\",\n    \"bot\",\n    \"natural language processing\",\n    \"nlp\",\n    \"artificial intelligence\",\n    \"ai\"\n]\nclassifiers = [\n    \"Development Status :: 4 - Beta\",\n    \"Intended Audience :: Developers\",\n    \"Operating System :: OS Independent\",\n    \"Environment :: Console\",\n    \"Environment :: Web Environment\",\n    \"Topic :: Internet\",\n    \"Topic :: Software Development :: Libraries\",\n    \"Topic :: Software Development :: Libraries :: Python Modules\",\n    \"Topic :: Scientific/Engineering :: Artificial Intelligence\",\n    \"Topic :: Scientific/Engineering :: Human Machine Interfaces\",\n    \"Topic :: Communications :: Chat\",\n    \"Topic :: Text Processing\",\n    \"Topic :: Text Processing :: Filters\",\n    \"Topic :: Text Processing :: General\",\n    \"Topic :: Text Processing :: Indexing\",\n    \"Topic :: Text Processing :: Linguistic\",\n    \"Programming Language :: Python\",\n    \"Programming Language :: Python :: 3\",\n    \"Programming Language :: Python :: 3.9\",\n    \"Programming Language :: Python :: 3 :: Only\",\n]\ndependencies = [\n    \"mathparse>=0.2,<0.3\",\n    \"python-dateutil>=2.9,<2.10\",\n    \"sqlalchemy>=2.0,<2.1\",\n    \"spacy>=3.8,<3.9\",\n    \"tqdm\",\n]\n\n[project.optional-dependencies]\ntest = [\n    \"flake8\",\n    \"coverage\",\n    \"sphinx>=5.3,<9.2\",\n    \"sphinx-sitemap>=2.6.0\",\n    \"huggingface_hub\",\n    \"django<=4.1,<6.0\"\n]\ndev = [\n    \"pint>=0.8.1\",\n    \"pyyaml>=6.0,<7.0\",\n    \"chatterbot-corpus>=1.2.2,<1.3.0\",\n    \"ollama>=0.6.0,<1.0\",\n    \"openai\"\n]\nredis = [\n    \"redis[hiredis]>=7.0,<7.2\",\n    \"langchain-redis<0.3.0\",\n    \"langchain-huggingface>=0.1.2,<1.3.0\",\n    \"accelerate>=1.6.0,<1.13\",\n    \"sentence-transformers>=4.0.2,<5.3.0\",\n]\nmongodb = [\n    \"pymongo>=4.11,<4.17\",\n]\n"
  },
  {
    "path": "setup.cfg",
    "content": "[flake8]\n# H306: imports not in alphabetical order (time, os)\n# W504: line break after binary operator (conflicts with W503, which is preferred)\nignore = H306, W504\nmax_line_length = 175\nexclude = .eggs, .git, build,\n"
  },
  {
    "path": "tests/__init__.py",
    "content": ""
  },
  {
    "path": "tests/base_case.py",
    "content": "from unittest import TestCase, SkipTest\nfrom chatterbot import ChatBot\nfrom chatterbot.conversation import Statement\n\n\nclass ChatBotTestCase(TestCase):\n    \"\"\"\n    Base test case class that provides common test utilities.\n    \"\"\"\n\n    # Share a single tagger instance across all tests in a test class to avoid\n    # repeatedly loading the spaCy model (saves 1-3 seconds per test)\n    _shared_tagger = None\n\n    @classmethod\n    def setUpClass(cls):\n        super().setUpClass()\n        if cls._shared_tagger is None:\n            from chatterbot.tagging import PosLemmaTagger\n            cls._shared_tagger = PosLemmaTagger()\n\n    def setUp(self):\n        kwargs = self.get_kwargs()\n        kwargs['tagger'] = self._shared_tagger\n        self.chatbot = ChatBot('Test Bot', **kwargs)\n\n    def _add_search_text(self, **kwargs):\n        \"\"\"\n        Return the search text for a statement.\n        \"\"\"\n\n        if 'text' in kwargs:\n            kwargs['search_text'] = self.chatbot.tagger.get_text_index_string(\n                kwargs['text']\n            )\n\n        if 'in_response_to' in kwargs:\n            kwargs['search_in_response_to'] = self.chatbot.tagger.get_text_index_string(\n                kwargs['in_response_to']\n            )\n\n        return Statement(**kwargs)\n\n    def _create_with_search_text(self, text, in_response_to=None, **kwargs):\n        \"\"\"\n        Helper function to create a statement with the search text populated.\n        \"\"\"\n        search_in_response_to = None\n\n        if in_response_to:\n            search_in_response_to = self.chatbot.tagger.get_text_index_string(\n                in_response_to\n            )\n\n        self.chatbot.storage.create(\n            text=text,\n            in_response_to=in_response_to,\n            search_text=self.chatbot.tagger.get_text_index_string(text),\n            search_in_response_to=search_in_response_to,\n            **kwargs\n        )\n\n    def _create_many_with_search_text(self, statements):\n        \"\"\"\n        Helper function to bulk-create statements with the search text populated.\n        \"\"\"\n        modified_statements = []\n\n        for statement in statements:\n            statement.search_text = self.chatbot.tagger.get_text_index_string(\n                statement.text\n            )\n\n            if statement.in_response_to:\n                statement.search_in_response_to = self.chatbot.tagger.get_text_index_string(\n                    statement.in_response_to\n                )\n\n            modified_statements.append(statement)\n\n        self.chatbot.storage.create_many(statements)\n\n    def tearDown(self):\n        \"\"\"\n        Remove the test database.\n        \"\"\"\n        self.chatbot.storage.drop()\n        self.chatbot.storage.close()\n\n    def assertIsLength(self, item, length):\n        \"\"\"\n        Assert that an iterable has the given length.\n        \"\"\"\n        if len(item) != length:\n            message = 'Length {} is not equal to {}'.format(len(item), length)\n            raise self.failureException(message)\n\n    def get_kwargs(self):\n        return {\n            # Run the test database in-memory\n            'database_uri': None,\n            # Don't execute initialization processes such as downloading required data\n            'initialize': False\n        }\n\n\nclass ChatBotMongoTestCase(ChatBotTestCase):\n\n    @classmethod\n    def setUpClass(cls):\n        from pymongo.errors import ServerSelectionTimeoutError\n        from pymongo import MongoClient\n\n        # Skip these tests if a mongo client is not running\n        try:\n            client = MongoClient(\n                serverSelectionTimeoutMS=0.1\n            )\n            client.server_info()\n\n            client.close()\n\n        except ServerSelectionTimeoutError:\n            raise SkipTest('Unable to connect to Mongo DB.')\n\n        # Initialize the shared tagger\n        super().setUpClass()\n\n    def get_kwargs(self):\n        kwargs = super().get_kwargs()\n        kwargs['database_uri'] = 'mongodb://localhost:27017/chatterbot_test_database'\n        kwargs['storage_adapter'] = 'chatterbot.storage.MongoDatabaseAdapter'\n        return kwargs\n\n\nclass ChatBotSQLTestCase(ChatBotTestCase):\n\n    def get_kwargs(self):\n        kwargs = super().get_kwargs()\n        kwargs['storage_adapter'] = 'chatterbot.storage.SQLStorageAdapter'\n        return kwargs\n"
  },
  {
    "path": "tests/django_integration/__init__.py",
    "content": "\"\"\"\nDjango integration tests for ChatterBot.\n\nThis package contains tests that require Django to be installed.\nIf Django is not available, these tests will be gracefully skipped.\n\"\"\"\n\nimport os\nimport sys\n\n# Check if Django is available\ntry:\n    import django\n    from django.core.management import call_command\n    DJANGO_AVAILABLE = True\n\n    # Configure Django settings immediately upon import\n    os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'tests.django_integration.test_settings')\n\n    try:\n        django.setup()\n\n        # Run migrations to create database tables\n        call_command('migrate', '--run-syncdb', verbosity=0, interactive=False)\n\n    except Exception as e:\n        print(f\"Warning: Django setup failed: {e}\", file=sys.stderr)\n        DJANGO_AVAILABLE = False\n\nexcept ImportError:\n    DJANGO_AVAILABLE = False\n\n\ndef load_tests(loader, tests, pattern):\n    \"\"\"\n    Custom test loader that configures Django before running tests.\n\n    This function is called automatically by unittest's discovery mechanism.\n    If Django is not installed, it returns an empty test suite.\n    \"\"\"\n    if not DJANGO_AVAILABLE:\n        # Return empty test suite if Django is not available\n        import unittest\n        return unittest.TestSuite()\n\n    # Load all tests from this package\n    package_tests = loader.discover(\n        start_dir=os.path.dirname(__file__),\n        pattern=pattern or 'test*.py',\n        top_level_dir=os.path.dirname(os.path.dirname(__file__))\n    )\n\n    return package_tests\n"
  },
  {
    "path": "tests/django_integration/base_case.py",
    "content": "from chatterbot import ChatBot\nfrom django.test import TransactionTestCase\nfrom tests.django_integration import test_settings\n\n\nclass ChatterBotTestCase(TransactionTestCase):\n    \"\"\"\n    Base test case for ChatterBot Django integration tests.\n    \"\"\"\n\n    def setUp(self):\n        \"\"\"\n        Set up a fresh ChatBot instance and clean database for each test.\n        \"\"\"\n        super().setUp()\n        self.chatbot = ChatBot(**test_settings.CHATTERBOT)\n\n    def _create_with_search_text(self, text, in_response_to=None, **kwargs):\n        \"\"\"\n        Helper function to create a statement with the search text populated.\n        \"\"\"\n        search_in_response_to = None\n\n        if in_response_to:\n            search_in_response_to = self.chatbot.tagger.get_text_index_string(\n                in_response_to\n            )\n\n        return self.chatbot.storage.create(\n            text=text,\n            in_response_to=in_response_to,\n            search_text=self.chatbot.tagger.get_text_index_string(text),\n            search_in_response_to=search_in_response_to,\n            **kwargs\n        )\n\n    def _create_many_with_search_text(self, statements):\n        \"\"\"\n        Helper function to bulk-create statements with the search text populated.\n        \"\"\"\n        modified_statements = []\n\n        for statement in statements:\n            statement.search_text = self.chatbot.tagger.get_text_index_string(\n                statement.text\n            )\n\n            if statement.in_response_to:\n                statement.search_in_response_to = self.chatbot.tagger.get_text_index_string(\n                    statement.in_response_to\n                )\n\n            modified_statements.append(statement)\n\n        self.chatbot.storage.create_many(statements)\n"
  },
  {
    "path": "tests/django_integration/test_chatbot.py",
    "content": "from tests.django_integration.base_case import ChatterBotTestCase\nfrom chatterbot.conversation import Statement\n\n\nclass ChatBotTests(ChatterBotTestCase):\n\n    def test_get_response_text(self):\n        self.chatbot.get_response(text='Test')\n\n    def test_no_statements_known(self):\n        \"\"\"\n        If there is no statements in the database, then the\n        user's input is the only thing that can be returned.\n        \"\"\"\n        statement_text = 'How are you?'\n        response = self.chatbot.get_response(statement_text)\n        results = list(self.chatbot.storage.filter(text=statement_text))\n\n        self.assertEqual(response.text, statement_text)\n        self.assertEqual(response.confidence, 0)\n\n        # Make sure that the input and output were saved\n        self.assertEqual(len(results), 2)\n        self.assertEqual(results[0].text, statement_text)\n        self.assertEqual(results[1].text, statement_text)\n\n    def test_one_statement_known_no_response(self):\n        \"\"\"\n        Test the case where a single statement is known, but\n        it is not in response to any other statement.\n        \"\"\"\n        self._create_with_search_text(text='Hello', in_response_to=None)\n\n        response = self.chatbot.get_response('Hi')\n\n        self.assertEqual(response.confidence, 0)\n        self.assertEqual(response.text, 'Hello')\n\n    def test_one_statement_one_response_known(self):\n        \"\"\"\n        Test the case that one response is known and there is a response\n        entry for it in the database.\n        \"\"\"\n        self._create_with_search_text(text='Hello', in_response_to='Hi')\n\n        response = self.chatbot.get_response('Hi')\n\n        self.assertEqual(response.confidence, 1)\n        self.assertEqual(response.text, 'Hello')\n\n    def test_two_statements_one_response_known(self):\n        \"\"\"\n        Test the case that one response is known and there is a response\n        entry for it in the database.\n        \"\"\"\n        self._create_with_search_text(text='Hi', in_response_to=None)\n        self._create_with_search_text(text='Hello', in_response_to='Hi')\n\n        response = self.chatbot.get_response('Hi')\n\n        self.assertEqual(response.confidence, 1)\n        self.assertEqual(response.text, 'Hello')\n\n    def test_three_statements_two_responses_known(self):\n        self._create_with_search_text(text='Hi', in_response_to=None, conversation='test')\n        self._create_with_search_text(text='Hello', in_response_to='Hi', conversation='test')\n        self._create_with_search_text(text='How are you?', in_response_to='Hello', conversation='test')\n\n        first_response = self.chatbot.get_response('Hi', conversation='test')\n        second_response = self.chatbot.get_response('How are you?', conversation='test')\n\n        self.assertEqual(first_response.confidence, 1)\n        self.assertEqual(first_response.text, 'Hello')\n        self.assertEqual(second_response.confidence, 1)\n\n    def test_four_statements_three_responses_known(self):\n        self._create_with_search_text(text='Hi', in_response_to=None, conversation='test')\n        self._create_with_search_text(text='Hello', in_response_to='Hi', conversation='test')\n        self._create_with_search_text(text='How are you?', in_response_to='Hello', conversation='test')\n        self._create_with_search_text(text='I am well.', in_response_to='How are you?', conversation='test')\n\n        first_response = self.chatbot.get_response('Hi', conversation='test')\n        second_response = self.chatbot.get_response('How are you?', conversation='test')\n\n        self.assertEqual(first_response.confidence, 1)\n        self.assertEqual(first_response.text, 'Hello')\n        self.assertEqual(second_response.confidence, 1)\n        self.assertEqual(second_response.text, 'I am well.')\n\n    def test_second_response_unknown(self):\n        self._create_with_search_text(text='Hi', in_response_to=None)\n        self._create_with_search_text(text='Hello', in_response_to='Hi')\n\n        first_response = self.chatbot.get_response(\n            text='Hi',\n            conversation='test'\n        )\n        second_response = self.chatbot.get_response(\n            text='How are you?',\n            conversation='test'\n        )\n\n        results = list(self.chatbot.storage.filter(text='How are you?'))\n\n        self.assertEqual(first_response.confidence, 1)\n        self.assertEqual(first_response.text, 'Hello')\n        self.assertEqual(first_response.in_response_to, 'Hi')\n\n        self.assertEqual(second_response.confidence, 0)\n        self.assertEqual(second_response.in_response_to, 'How are you?')\n\n        # Make sure that the second response was saved to the database\n        self.assertEqual(len(results), 1)\n        self.assertEqual(results[0].in_response_to, 'Hello')\n\n    def test_statement_added_to_conversation(self):\n        \"\"\"\n        An input statement should be added to the recent response list.\n        \"\"\"\n        statement = Statement(text='Wow!', conversation='test')\n        response = self.chatbot.get_response(statement)\n\n        self.assertEqual(statement.text, response.text)\n        self.assertEqual(response.conversation, 'test')\n\n    def test_get_response_additional_response_selection_parameters(self):\n        self._create_many_with_search_text([\n            Statement('A', conversation='test_1'),\n            Statement('B', conversation='test_1', in_response_to='A'),\n            Statement('A', conversation='test_2'),\n            Statement('C', conversation='test_2', in_response_to='A'),\n        ])\n\n        statement = Statement(text='A', conversation='test_3')\n        response = self.chatbot.get_response(statement, additional_response_selection_parameters={\n            'conversation': 'test_2'\n        })\n\n        self.assertEqual(response.text, 'C')\n        self.assertEqual(response.conversation, 'test_3')\n\n    def test_get_response_unicode(self):\n        \"\"\"\n        Test the case that a unicode string is passed in.\n        \"\"\"\n        response = self.chatbot.get_response(u'سلام')\n        self.assertGreater(len(response.text), 0)\n\n    def test_get_response_emoji(self):\n        \"\"\"\n        Test the case that the input string contains an emoji.\n        \"\"\"\n        response = self.chatbot.get_response(u'💩 ')\n        self.assertGreater(len(response.text), 0)\n\n    def test_get_response_non_whitespace(self):\n        \"\"\"\n        Test the case that a non-whitespace C1 control string is passed in.\n        \"\"\"\n        response = self.chatbot.get_response(u'')\n        self.assertGreater(len(response.text), 0)\n\n    def test_get_response_two_byte_characters(self):\n        \"\"\"\n        Test the case that a string containing two-byte characters is passed in.\n        \"\"\"\n        response = self.chatbot.get_response(u'田中さんにあげて下さい')\n        self.assertGreater(len(response.text), 0)\n\n    def test_get_response_corrupted_text(self):\n        \"\"\"\n        Test the case that a string contains \"corrupted\" text.\n        \"\"\"\n        response = self.chatbot.get_response(u'Ṱ̺̺̕h̼͓̲̦̳̘̲e͇̣̰̦̬͎ ̢̼̻̱̘h͚͎͙̜̣̲ͅi̦̲̣̰̤v̻͍e̺̭̳̪̰-m̢iͅn̖̺̞̲̯̰d̵̼̟͙̩̼̘̳.̨̹͈̣')\n        self.assertGreater(len(response.text), 0)\n\n    def test_response_with_tags_added(self):\n        \"\"\"\n        If an input statement has tags added to it,\n        that data should saved with the input statement.\n        \"\"\"\n        self.chatbot.get_response(Statement(\n            text='Hello',\n            in_response_to='Hi',\n            tags=['test']\n        ))\n\n        results = list(self.chatbot.storage.filter(text='Hello'))\n\n        self.assertEqual(len(results), 2)\n        self.assertIn('test', results[0].get_tags())\n        self.assertEqual(results[1].get_tags(), ['test'])\n\n    def test_get_response_with_text_and_kwargs(self):\n        self.chatbot.get_response('Hello', conversation='greetings')\n\n        results = list(self.chatbot.storage.filter(text='Hello'))\n\n        self.assertEqual(len(results), 2)\n        self.assertEqual(results[0].conversation, 'greetings')\n        self.assertEqual(results[1].conversation, 'greetings')\n\n    def test_get_response_missing_text(self):\n        with self.assertRaises(self.chatbot.ChatBotException):\n            self.chatbot.get_response()\n\n    def test_get_response_missing_text_with_conversation(self):\n        with self.assertRaises(self.chatbot.ChatBotException):\n            self.chatbot.get_response(conversation='test')\n\n    def test_generate_response(self):\n        statement = Statement(text='Many insects adopt a tripedal gait for rapid yet stable walking.')\n        response = self.chatbot.generate_response(statement)\n\n        self.assertEqual(response.text, statement.text)\n        self.assertEqual(response.confidence, 0)\n\n    def test_learn_response(self):\n        previous_response = Statement(text='Define Hemoglobin.')\n        statement = Statement(text='Hemoglobin is an oxygen-transport metalloprotein.')\n        self.chatbot.learn_response(statement, previous_response)\n        results = list(self.chatbot.storage.filter(text=statement.text))\n\n        self.assertEqual(len(results), 1)\n\n    def test_get_response_does_not_add_new_statement(self):\n        \"\"\"\n        Test that a new statement is not learned if `read_only` is set to True.\n        \"\"\"\n        self.chatbot.read_only = True\n        self.chatbot.get_response('Hi!')\n        results = list(self.chatbot.storage.filter(text='Hi!'))\n\n        self.assertEqual(len(results), 0)\n\n    def test_get_latest_response_from_zero_responses(self):\n        response = self.chatbot.get_latest_response('invalid')\n\n        self.assertIsNone(response)\n\n    def test_get_latest_response_from_one_responses(self):\n        self._create_with_search_text(text='A', conversation='test')\n        self._create_with_search_text(text='B', conversation='test', in_response_to='A')\n\n        response = self.chatbot.get_latest_response('test')\n\n        self.assertEqual(response.text, 'B')\n\n    def test_get_latest_response_from_two_responses(self):\n        self._create_with_search_text(text='A', conversation='test')\n        self._create_with_search_text(text='B', conversation='test', in_response_to='A')\n        self._create_with_search_text(text='C', conversation='test', in_response_to='B')\n\n        response = self.chatbot.get_latest_response('test')\n\n        self.assertEqual(response.text, 'C')\n\n    def test_get_latest_response_from_three_responses(self):\n        self._create_with_search_text(text='A', conversation='test')\n        self._create_with_search_text(text='B', conversation='test', in_response_to='A')\n        self._create_with_search_text(text='C', conversation='test', in_response_to='B')\n        self._create_with_search_text(text='D', conversation='test', in_response_to='C')\n\n        response = self.chatbot.get_latest_response('test')\n\n        self.assertEqual(response.text, 'D')\n\n    def test_search_text_results_after_training(self):\n        \"\"\"\n        ChatterBot should return close matches to an input\n        string when filtering using the search_text parameter.\n        \"\"\"\n        self._create_many_with_search_text([\n            Statement('Example A for search.'),\n            Statement('Another example.'),\n            Statement('Example B for search.'),\n            Statement(text='Another statement.'),\n        ])\n\n        results = list(self.chatbot.storage.filter(\n            search_text=self.chatbot.tagger.get_text_index_string(\n                'Example A for search.'\n            )\n        ))\n\n        self.assertEqual(len(results), 1, msg=[r.text for r in results])\n        self.assertEqual('Example A for search.', results[0].text)\n\n    def test_search_text_contains_results_after_training(self):\n        \"\"\"\n        ChatterBot should return close matches to an input\n        string when filtering using the search_text parameter.\n        \"\"\"\n        self._create_many_with_search_text([\n            Statement('Example A for search.'),\n            Statement('Another example.'),\n            Statement('Example B for search.'),\n            Statement(text='Another statement.'),\n        ])\n\n        results = list(self.chatbot.storage.filter(\n            search_text_contains=self.chatbot.tagger.get_text_index_string(\n                'Example A for search.'\n            )\n        ))\n\n        self.assertEqual(len(results), 2, msg=[r.text for r in results])\n        self.assertEqual('Example A for search.', results[0].text)\n        self.assertEqual('Example B for search.', results[1].text)\n"
  },
  {
    "path": "tests/django_integration/test_chatterbot_corpus_training.py",
    "content": "from tests.django_integration.base_case import ChatterBotTestCase\nfrom chatterbot.trainers import ChatterBotCorpusTrainer\n\n\nclass ChatterBotCorpusTrainingTestCase(ChatterBotTestCase):\n    \"\"\"\n    Test case for training with data from the ChatterBot Corpus.\n\n    Note: This class has a mirror tests/training_tests/\n    \"\"\"\n\n    def setUp(self):\n        super().setUp()\n\n        self.trainer = ChatterBotCorpusTrainer(\n            self.chatbot,\n            show_training_progress=False\n        )\n\n    def tearDown(self):\n        super().tearDown()\n        self.chatbot.storage.drop()\n\n    def test_train_with_english_greeting_corpus(self):\n        self.trainer.train('chatterbot.corpus.english.greetings')\n\n        results = list(self.chatbot.storage.filter(text='Hello'))\n\n        self.assertGreater(len(results), 1)\n\n    def test_train_with_english_greeting_corpus_tags(self):\n        self.trainer.train('chatterbot.corpus.english.greetings')\n\n        results = list(self.chatbot.storage.filter(text='Hello'))\n\n        self.assertGreater(len(results), 1)\n        statement = results[0]\n        self.assertEqual(['greetings'], statement.get_tags())\n\n    def test_train_with_multiple_corpora(self):\n        self.trainer.train(\n            'chatterbot.corpus.english.greetings',\n            'chatterbot.corpus.english.conversations',\n        )\n        results = list(self.chatbot.storage.filter(text='Hello'))\n\n        self.assertGreater(len(results), 1)\n\n    def test_train_with_english_corpus(self):\n        self.trainer.train('chatterbot.corpus.english')\n        results = list(self.chatbot.storage.filter(text='Hello'))\n\n        self.assertGreater(len(results), 1)\n"
  },
  {
    "path": "tests/django_integration/test_chatterbot_settings.py",
    "content": "from django.test import TestCase\nfrom django.conf import settings\n\n\nclass SettingsTestCase(TestCase):\n\n    def test_modified_settings(self):\n        with self.settings(CHATTERBOT={'name': 'Jim'}):\n            self.assertIn('name', settings.CHATTERBOT)\n            self.assertEqual('Jim', settings.CHATTERBOT['name'])\n\n    def test_name_setting(self):\n        with self.settings():\n            self.assertIn('name', settings.CHATTERBOT)\n            self.assertEqual('ChatterBot', settings.CHATTERBOT['name'])\n"
  },
  {
    "path": "tests/django_integration/test_custom_models.py",
    "content": "\"\"\"\nTests for custom model swapping functionality.\n\"\"\"\nfrom tests.django_integration.base_case import ChatterBotTestCase\nfrom django.test import override_settings\nfrom chatterbot import ChatBot\n\n\nclass CustomModelsTestCase(ChatterBotTestCase):\n    \"\"\"\n    Test custom model configuration via settings and kwargs.\n    \"\"\"\n\n    def test_default_models_used_without_settings(self):\n        \"\"\"\n        Test that default models are used when no custom settings provided.\n        \"\"\"\n        chatbot = ChatBot(\n            'Test Bot',\n            storage_adapter='chatterbot.storage.DjangoStorageAdapter'\n        )\n\n        # Should use default django_chatterbot.Statement model\n        self.assertEqual(\n            chatbot.storage.statement_model,\n            'django_chatterbot.Statement'\n        )\n        self.assertEqual(\n            chatbot.storage.tag_model,\n            'django_chatterbot.Tag'\n        )\n\n    def test_custom_models_via_kwargs(self):\n        \"\"\"\n        Test that custom models can be specified via kwargs.\n        \"\"\"\n        chatbot = ChatBot(\n            'Test Bot',\n            storage_adapter='chatterbot.storage.DjangoStorageAdapter',\n            statement_model='myapp.CustomStatement',\n            tag_model='myapp.CustomTag'\n        )\n\n        self.assertEqual(chatbot.storage.statement_model, 'myapp.CustomStatement')\n        self.assertEqual(chatbot.storage.tag_model, 'myapp.CustomTag')\n\n    @override_settings(\n        CHATTERBOT_STATEMENT_MODEL='myapp.CustomStatement',\n        CHATTERBOT_TAG_MODEL='myapp.CustomTag'\n    )\n    def test_custom_models_via_settings(self):\n        \"\"\"\n        Test that custom models can be specified via Django settings.\n        \"\"\"\n        chatbot = ChatBot(\n            'Test Bot',\n            storage_adapter='chatterbot.storage.DjangoStorageAdapter'\n        )\n\n        # Should read from Django settings\n        self.assertEqual(chatbot.storage.statement_model, 'myapp.CustomStatement')\n        self.assertEqual(chatbot.storage.tag_model, 'myapp.CustomTag')\n\n    def test_kwargs_override_settings(self):\n        \"\"\"\n        Test that kwargs take precedence over Django settings.\n        \"\"\"\n        with override_settings(\n            CHATTERBOT_STATEMENT_MODEL='settings.StatementModel',\n            CHATTERBOT_TAG_MODEL='settings.TagModel'\n        ):\n            chatbot = ChatBot(\n                'Test Bot',\n                storage_adapter='chatterbot.storage.DjangoStorageAdapter',\n                statement_model='kwargs.StatementModel',\n                tag_model='kwargs.TagModel'\n            )\n\n            # kwargs should take precedence over settings\n            self.assertEqual(chatbot.storage.statement_model, 'kwargs.StatementModel')\n            self.assertEqual(chatbot.storage.tag_model, 'kwargs.TagModel')\n\n    def test_get_statement_model(self):\n        \"\"\"\n        Test that get_statement_model() returns the correct model class.\n        \"\"\"\n        chatbot = ChatBot(\n            'Test Bot',\n            storage_adapter='chatterbot.storage.DjangoStorageAdapter'\n        )\n\n        Statement = chatbot.storage.get_statement_model()\n\n        # Should be the default Statement model\n        self.assertEqual(Statement.__name__, 'Statement')\n        self.assertEqual(Statement._meta.app_label, 'django_chatterbot')\n\n    def test_get_tag_model(self):\n        \"\"\"\n        Test that get_tag_model() returns the correct model class.\n        \"\"\"\n        chatbot = ChatBot(\n            'Test Bot',\n            storage_adapter='chatterbot.storage.DjangoStorageAdapter'\n        )\n\n        Tag = chatbot.storage.get_tag_model()\n\n        # Should be the default Tag model\n        self.assertEqual(Tag.__name__, 'Tag')\n        self.assertEqual(Tag._meta.app_label, 'django_chatterbot')\n\n    def test_model_configuration_persists(self):\n        \"\"\"\n        Test that model configuration is stored correctly on the adapter instance.\n        \"\"\"\n        chatbot = ChatBot(\n            'Test Bot',\n            storage_adapter='chatterbot.storage.DjangoStorageAdapter',\n            statement_model='app1.Model1',\n            tag_model='app2.Model2'\n        )\n\n        # Configuration should persist\n        self.assertEqual(chatbot.storage.statement_model, 'app1.Model1')\n        self.assertEqual(chatbot.storage.tag_model, 'app2.Model2')\n\n        # Should be the same after multiple accesses\n        self.assertEqual(chatbot.storage.statement_model, 'app1.Model1')\n        self.assertEqual(chatbot.storage.tag_model, 'app2.Model2')\n"
  },
  {
    "path": "tests/django_integration/test_django_adapter.py",
    "content": "from django.test import TestCase\nfrom chatterbot.storage import DjangoStorageAdapter\nfrom chatterbot.conversation import Statement as StatementObject\nfrom chatterbot.ext.django_chatterbot.models import Statement\n\n\nclass DjangoAdapterTestCase(TestCase):\n\n    def setUp(self):\n        \"\"\"\n        Instantiate the adapter.\n        \"\"\"\n        self.adapter = DjangoStorageAdapter()\n\n    def tearDown(self):\n        \"\"\"\n        Remove the test database.\n        \"\"\"\n        self.adapter.drop()\n\n\nclass DjangoStorageAdapterTests(DjangoAdapterTestCase):\n\n    def test_count_returns_zero(self):\n        \"\"\"\n        The count method should return a value of 0\n        when nothing has been saved to the database.\n        \"\"\"\n        self.assertEqual(self.adapter.count(), 0)\n\n    def test_count_returns_value(self):\n        \"\"\"\n        The count method should return a value of 1\n        when one item has been saved to the database.\n        \"\"\"\n        self.adapter.create(text=\"Test statement\")\n        self.assertEqual(self.adapter.count(), 1)\n\n    def test_filter_statement_not_found(self):\n        \"\"\"\n        Test that None is returned by the find method\n        when a matching statement is not found.\n        \"\"\"\n        results = list(self.adapter.filter(text=\"Non-existent\"))\n        self.assertEqual(len(results), 0)\n\n    def test_filter_statement_found(self):\n        \"\"\"\n        Test that a matching statement is returned\n        when it exists in the database.\n        \"\"\"\n        statement = self.adapter.create(text=\"New statement\")\n\n        results = list(self.adapter.filter(text=\"New statement\"))\n\n        self.assertEqual(len(results), 1)\n        self.assertEqual(results[0].text, statement.text)\n\n    def test_update_adds_new_statement(self):\n        statement = Statement(text=\"New statement\")\n        self.adapter.update(statement)\n\n        results = list(self.adapter.filter(text=\"New statement\"))\n\n        self.assertEqual(len(results), 1)\n        self.assertEqual(results[0].text, statement.text)\n\n    def test_update_modifies_existing_statement(self):\n        statement = self.adapter.create(text=\"New statement\")\n        other_statement = self.adapter.create(text=\"New response\")\n\n        # Check the initial values\n        results = list(self.adapter.filter(text=statement.text))\n\n        self.assertEqual(results[0].in_response_to, None)\n\n        statement.in_response_to = other_statement.text\n\n        # Update the statement value\n        self.adapter.update(statement)\n\n        # Check that the values have changed\n        results = list(self.adapter.filter(text=statement.text))\n\n        self.assertEqual(results[0].in_response_to, other_statement.text)\n\n    def test_get_random_returns_statement(self):\n        statement = self.adapter.create(text=\"New statement\")\n\n        random_statement = self.adapter.get_random()\n        self.assertEqual(random_statement.text, statement.text)\n\n    def test_get_random_no_data(self):\n        from chatterbot.storage import StorageAdapter\n\n        with self.assertRaises(StorageAdapter.EmptyDatabaseException):\n            self.adapter.get_random()\n\n    def test_filter_by_text_multiple_results(self):\n        self.adapter.create(\n            text=\"Do you like this?\",\n            in_response_to=\"Yes\"\n        )\n        self.adapter.create(\n            text=\"Do you like this?\",\n            in_response_to=\"No\"\n        )\n\n        results = list(self.adapter.filter(text=\"Do you like this?\"))\n\n        self.assertEqual(len(results), 2)\n\n    def test_remove(self):\n        text = \"Sometimes you have to run before you can walk.\"\n        statement = self.adapter.create(text=text)\n\n        self.adapter.remove(statement.text)\n        results = list(self.adapter.filter(text=text))\n\n        self.assertEqual(len(results), 0)\n\n    def test_remove_response(self):\n        text = \"Sometimes you have to run before you can walk.\"\n        statement = self.adapter.create(text=text)\n        self.adapter.remove(statement.text)\n        results = list(self.adapter.filter(text=text))\n\n        self.assertEqual(len(results), 0)\n\n\nclass DjangoAdapterFilterTests(DjangoAdapterTestCase):\n\n    def test_filter_text_no_matches(self):\n        self.adapter.create(\n            text='Testing...',\n            in_response_to='Why are you counting?'\n        )\n        results = list(self.adapter.filter(text=\"Howdy\"))\n\n        self.assertEqual(len(results), 0)\n\n    def test_filter_in_response_to_no_matches(self):\n        self.adapter.create(\n            text='Testing...',\n            in_response_to='Why are you counting?'\n        )\n\n        results = list(self.adapter.filter(in_response_to=\"Maybe\"))\n\n        self.assertEqual(len(results), 0)\n\n    def test_filter_equal_results(self):\n        statement1 = self.adapter.create(text=\"Testing...\")\n        statement2 = self.adapter.create(text=\"Testing one, two, three.\")\n\n        results = list(self.adapter.filter(in_response_to=None))\n\n        self.assertEqual(len(results), 2)\n\n        text_for_statements = [\n            statement.text for statement in results\n        ]\n        self.assertIn(statement1.text, text_for_statements)\n        self.assertIn(statement2.text, text_for_statements)\n\n    def test_filter_contains_result(self):\n        self.adapter.create(\n            text='Testing...',\n            in_response_to='Why are you counting?'\n        )\n        self.adapter.create(\n            text='Testing one, two, three.',\n            in_response_to='Testing...'\n        )\n\n        results = list(self.adapter.filter(\n            in_response_to=\"Why are you counting?\"\n        ))\n        self.assertEqual(len(results), 1)\n        self.assertEqual(results[0].text, 'Testing...')\n\n    def test_filter_contains_no_result(self):\n        self.adapter.create(\n            text='Testing...',\n            in_response_to='Why are you counting?'\n        )\n\n        results = list(self.adapter.filter(\n            in_response_to=\"How do you do?\"\n        ))\n        self.assertEqual(len(results), 0)\n\n    def test_filter_no_parameters(self):\n        \"\"\"\n        If no parameters are passed to the filter,\n        then all statements should be returned.\n        \"\"\"\n        self.adapter.create(text=\"Testing...\")\n        self.adapter.create(text=\"Testing one, two, three.\")\n\n        results = list(self.adapter.filter())\n\n        self.assertEqual(len(results), 2)\n\n    def test_filter_by_tag(self):\n        self.adapter.create(text=\"Hello!\", tags=[\"greeting\", \"salutation\"])\n        self.adapter.create(text=\"Hi everyone!\", tags=[\"greeting\", \"exclamation\"])\n        self.adapter.create(text=\"The air contains Oxygen.\", tags=[\"fact\"])\n\n        results = list(self.adapter.filter(tags=[\"greeting\"]))\n\n        results_text_list = [statement.text for statement in results]\n\n        self.assertEqual(len(results_text_list), 2)\n        self.assertIn(\"Hello!\", results_text_list)\n        self.assertIn(\"Hi everyone!\", results_text_list)\n\n    def test_filter_by_tags(self):\n        self.adapter.create(text=\"Hello!\", tags=[\"greeting\", \"salutation\"])\n        self.adapter.create(text=\"Hi everyone!\", tags=[\"greeting\", \"exclamation\"])\n        self.adapter.create(text=\"The air contains Oxygen.\", tags=[\"fact\"])\n\n        results = list(self.adapter.filter(\n            tags=[\"exclamation\", \"fact\"]\n        ))\n\n        results_text_list = [statement.text for statement in results]\n\n        self.assertEqual(len(results_text_list), 2)\n        self.assertIn(\"Hi everyone!\", results_text_list)\n        self.assertIn(\"The air contains Oxygen.\", results_text_list)\n\n    def test_filter_page_size(self):\n        self.adapter.create(text='A')\n        self.adapter.create(text='B')\n        self.adapter.create(text='C')\n\n        results = self.adapter.filter(page_size=2)\n\n        results_text_list = [statement.text for statement in results]\n\n        self.assertEqual(len(results_text_list), 3)\n        self.assertIn('A', results_text_list)\n        self.assertIn('B', results_text_list)\n        self.assertIn('C', results_text_list)\n\n    def test_confidence(self):\n        \"\"\"\n        Test that the confidence value is not saved to the database.\n        The confidence attribute on statements is intended to just hold\n        the confidence of the statement when it returned as a response to\n        some input. Because of that, the value of the confidence score\n        should never be stored in the database with the statement.\n        \"\"\"\n        statement = self.adapter.create(text='Test statement')\n        statement.confidence = 0.5\n\n        statement_updated = Statement.objects.get(pk=statement.id)\n\n        self.assertEqual(statement_updated.confidence, 0)\n\n    def test_exclude_text(self):\n        self.adapter.create(text='Hello!')\n        self.adapter.create(text='Hi everyone!')\n\n        results = list(self.adapter.filter(\n            exclude_text=[\n                'Hello!'\n            ]\n        ))\n\n        self.assertEqual(len(results), 1)\n        self.assertEqual(results[0].text, 'Hi everyone!')\n\n    def test_exclude_text_words(self):\n        self.adapter.create(text='This is a good example.')\n        self.adapter.create(text='This is a bad example.')\n        self.adapter.create(text='This is a worse example.')\n\n        results = list(self.adapter.filter(\n            exclude_text_words=[\n                'bad', 'worse'\n            ]\n        ))\n\n        self.assertEqual(len(results), 1)\n        self.assertEqual(results[0].text, 'This is a good example.')\n\n    def test_persona_not_startswith(self):\n        self.adapter.create(text='Hello!', persona='bot:tester')\n        self.adapter.create(text='Hi everyone!', persona='user:person')\n\n        results = list(self.adapter.filter(\n            persona_not_startswith='bot:'\n        ))\n\n        self.assertEqual(len(results), 1)\n        self.assertEqual(results[0].text, 'Hi everyone!')\n\n    def test_search_text_contains(self):\n        self.adapter.create(text='Hello!', search_text='hello exclamation')\n        self.adapter.create(text='Hi everyone!', search_text='hi everyone')\n\n        results = list(self.adapter.filter(\n            search_text_contains='everyone'\n        ))\n\n        self.assertEqual(len(results), 1)\n        self.assertEqual(results[0].text, 'Hi everyone!')\n\n    def test_search_text_contains_multiple_matches(self):\n        self.adapter.create(text='Hello!', search_text='hello exclamation')\n        self.adapter.create(text='Hi everyone!', search_text='hi everyone')\n\n        results = list(self.adapter.filter(\n            search_text_contains='hello everyone'\n        ))\n\n        self.assertEqual(len(results), 2)\n\n\nclass DjangoOrderingTests(DjangoAdapterTestCase):\n    \"\"\"\n    Test cases for the ordering of sets of statements.\n    \"\"\"\n\n    def test_order_by_text(self):\n        statement_a = self.adapter.create(text='A is the first letter of the alphabet.')\n        statement_b = self.adapter.create(text='B is the second letter of the alphabet.')\n\n        results = list(self.adapter.filter(order_by=['text']))\n\n        self.assertEqual(len(results), 2)\n        self.assertEqual(results[0], statement_a)\n        self.assertEqual(results[1], statement_b)\n\n    def test_reverse_order_by_text(self):\n        statement_a = self.adapter.create(text='A is the first letter of the alphabet.')\n        statement_b = self.adapter.create(text='B is the second letter of the alphabet.')\n\n        results = list(self.adapter.filter(order_by=['-text']))\n\n        self.assertEqual(len(results), 2)\n        self.assertEqual(results[1], statement_a)\n        self.assertEqual(results[0], statement_b)\n\n\nclass StorageAdapterCreateTests(DjangoAdapterTestCase):\n    \"\"\"\n    Tests for the create function of the storage adapter.\n    \"\"\"\n\n    def test_create_text(self):\n        self.adapter.create(text='testing')\n\n        results = list(self.adapter.filter())\n\n        self.assertEqual(len(results), 1)\n        self.assertEqual(results[0].text, 'testing')\n\n    def test_create_search_text(self):\n        self.adapter.create(\n            text='testing',\n            search_text='test'\n        )\n\n        results = list(self.adapter.filter())\n\n        self.assertEqual(len(results), 1)\n        self.assertEqual(results[0].search_text, 'test')\n\n    def test_create_search_in_response_to(self):\n        self.adapter.create(\n            text='testing',\n            search_in_response_to='test'\n        )\n\n        results = list(self.adapter.filter())\n\n        self.assertEqual(len(results), 1)\n        self.assertEqual(results[0].search_in_response_to, 'test')\n\n    def test_create_tags(self):\n        self.adapter.create(text='testing', tags=['a', 'b'])\n\n        results = list(self.adapter.filter())\n\n        self.assertEqual(len(results), 1)\n        self.assertIn('a', results[0].get_tags())\n        self.assertIn('b', results[0].get_tags())\n\n    def test_create_duplicate_tags(self):\n        \"\"\"\n        The storage adapter should not create a statement with tags\n        that are duplicates.\n        \"\"\"\n        self.adapter.create(text='testing', tags=['ab', 'ab'])\n\n        results = list(self.adapter.filter())\n\n        self.assertEqual(len(results), 1)\n        self.assertEqual(len(results[0].get_tags()), 1)\n        self.assertEqual(results[0].get_tags(), ['ab'])\n\n    def test_create_many_text(self):\n        self.adapter.create_many([\n            StatementObject(text='A'),\n            StatementObject(text='B')\n        ])\n\n        results = list(self.adapter.filter())\n\n        self.assertEqual(len(results), 2)\n        self.assertEqual(results[0].text, 'A')\n        self.assertEqual(results[1].text, 'B')\n\n    def test_create_many_search_text(self):\n        self.adapter.create_many([\n            StatementObject(text='A', search_text='a'),\n            StatementObject(text='B', search_text='b')\n        ])\n\n        results = list(self.adapter.filter())\n\n        self.assertEqual(len(results), 2)\n        self.assertEqual(results[0].search_text, 'a')\n        self.assertEqual(results[1].search_text, 'b')\n\n    def test_create_many_search_in_response_to(self):\n        self.adapter.create_many([\n            StatementObject(text='A', search_in_response_to='a'),\n            StatementObject(text='B', search_in_response_to='b')\n        ])\n\n        results = list(self.adapter.filter())\n\n        self.assertEqual(len(results), 2)\n        self.assertEqual(results[0].search_in_response_to, 'a')\n        self.assertEqual(results[1].search_in_response_to, 'b')\n\n    def test_create_many_tags(self):\n        self.adapter.create_many([\n            StatementObject(text='A', tags=['first', 'letter']),\n            StatementObject(text='B', tags=['second', 'letter'])\n        ])\n        results = list(self.adapter.filter())\n\n        self.assertEqual(len(results), 2)\n        self.assertIn('letter', results[0].get_tags())\n        self.assertIn('letter', results[1].get_tags())\n        self.assertIn('first', results[0].get_tags())\n        self.assertIn('second', results[1].get_tags())\n\n    def test_create_many_duplicate_tags(self):\n        \"\"\"\n        The storage adapter should not create a statement with tags\n        that are duplicates.\n        \"\"\"\n        self.adapter.create_many([\n            StatementObject(text='testing', tags=['ab', 'ab'])\n        ])\n\n        results = list(self.adapter.filter())\n\n        self.assertEqual(len(results), 1)\n        self.assertEqual(len(results[0].get_tags()), 1)\n        self.assertEqual(results[0].get_tags(), ['ab'])\n\n\nclass StorageAdapterUpdateTests(DjangoAdapterTestCase):\n    \"\"\"\n    Tests for the update function of the storage adapter.\n    \"\"\"\n\n    def test_update_adds_tags(self):\n        statement = self.adapter.create(text='Testing')\n        statement.add_tags('a', 'b')\n        self.adapter.update(statement)\n\n        statements = list(self.adapter.filter())\n\n        self.assertEqual(len(statements), 1)\n        self.assertIn('a', statements[0].get_tags())\n        self.assertIn('b', statements[0].get_tags())\n\n    def test_update_duplicate_tags(self):\n        \"\"\"\n        The storage adapter should not update a statement with tags\n        that are duplicates.\n        \"\"\"\n        statement = self.adapter.create(text='Testing', tags=['ab'])\n        statement.add_tags('ab')\n        self.adapter.update(statement)\n\n        statements = list(self.adapter.filter())\n\n        self.assertEqual(len(statements), 1)\n        self.assertEqual(len(statements[0].get_tags()), 1)\n        self.assertEqual(statements[0].get_tags(), ['ab'])\n"
  },
  {
    "path": "tests/django_integration/test_logic_adapter_integration.py",
    "content": "from tests.django_integration.base_case import ChatterBotTestCase\nfrom chatterbot.conversation import Statement\n\n\nclass LogicIntegrationTestCase(ChatterBotTestCase):\n    \"\"\"\n    Tests to make sure that logic adapters\n    function correctly when using Django.\n    \"\"\"\n\n    def setUp(self):\n        super().setUp()\n\n        self._create_with_search_text(text='Default statement')\n\n    def test_best_match(self):\n        from chatterbot.logic import BestMatch\n\n        adapter = BestMatch(self.chatbot)\n\n        statement1 = self._create_with_search_text(\n            text='Do you like programming?',\n            conversation='test'\n        )\n\n        self._create_with_search_text(\n            text='Yes',\n            in_response_to=statement1.text,\n            conversation='test'\n        )\n\n        response = adapter.process(statement1)\n\n        self.assertEqual(response.text, 'Yes')\n        self.assertEqual(response.confidence, 1)\n\n    def test_mathematical_evaluation(self):\n        from chatterbot.logic import MathematicalEvaluation\n\n        adapter = MathematicalEvaluation(self.chatbot)\n\n        statement = Statement(text='What is 6 + 6?')\n\n        response = adapter.process(statement)\n\n        self.assertEqual(response.text, '6 + 6 = 12')\n        self.assertEqual(response.confidence, 1)\n\n    def test_time(self):\n        from chatterbot.logic import TimeLogicAdapter\n\n        adapter = TimeLogicAdapter(self.chatbot)\n\n        statement = Statement(text='What time is it?')\n\n        response = adapter.process(statement)\n\n        self.assertIn('The current time is', response.text)\n        self.assertEqual(response.confidence, 1)\n"
  },
  {
    "path": "tests/django_integration/test_secondary_database.py",
    "content": "\"\"\"\nTests for using DjangoStorageAdapter with a secondary database.\n\"\"\"\nfrom tests.django_integration.base_case import ChatterBotTestCase\nfrom chatterbot import ChatBot\n\n\nclass SecondaryDatabaseTestCase(ChatterBotTestCase):\n    \"\"\"\n    Test that the database parameter can be passed to DjangoStorageAdapter.\n    \"\"\"\n\n    def setUp(self):\n        super().setUp()\n\n        # Create a chatbot that explicitly uses the 'default' database\n        self.chatbot = ChatBot(\n            'Test Bot',\n            storage_adapter='chatterbot.storage.DjangoStorageAdapter',\n            database='default',\n            logic_adapters=[\n                'chatterbot.logic.BestMatch'\n            ]\n        )\n\n    def test_database_parameter_accepted(self):\n        \"\"\"\n        Test that the database parameter is accepted and stored.\n        \"\"\"\n        self.assertEqual(self.chatbot.storage.database, 'default')\n\n    def test_database_parameter_default(self):\n        \"\"\"\n        Test that the database parameter defaults to 'default' if not specified.\n        \"\"\"\n        chatbot = ChatBot(\n            'Test Bot 2',\n            storage_adapter='chatterbot.storage.DjangoStorageAdapter'\n        )\n        self.assertEqual(chatbot.storage.database, 'default')\n\n    def test_operations_with_database_parameter(self):\n        \"\"\"\n        Test that basic operations work with the database parameter set.\n        \"\"\"\n        # Create a statement\n        statement = self.chatbot.storage.create(\n            text='Hello',\n            conversation='test'\n        )\n\n        self.assertIsNotNone(statement)\n        self.assertEqual(statement.text, 'Hello')\n\n        # Count statements\n        count = self.chatbot.storage.count()\n        self.assertGreater(count, 0)\n\n        # Filter statements\n        results = list(self.chatbot.storage.filter(text='Hello'))\n        self.assertEqual(len(results), 1)\n        self.assertEqual(results[0].text, 'Hello')\n\n        # Remove statement\n        self.chatbot.storage.remove('Hello')\n\n        # Verify removal\n        count_after = self.chatbot.storage.count()\n        self.assertEqual(count_after, count - 1)\n"
  },
  {
    "path": "tests/django_integration/test_settings.py",
    "content": "\"\"\"\nDjango settings for when tests are run.\n\"\"\"\nimport os\nfrom chatterbot import constants\n\nDEBUG = True\n\nBASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))\n\nSECRET_KEY = 'fake-key'\n\nINSTALLED_APPS = [\n    'django.contrib.auth',\n    'django.contrib.contenttypes',\n    'django.contrib.sessions',\n    'chatterbot.ext.django_chatterbot',\n    'tests.django_integration',\n]\n\nCHATTERBOT = {\n    'name': 'Test Django ChatterBot',\n    'logic_adapters': [\n        {\n            'import_path': 'chatterbot.logic.BestMatch',\n        },\n        {\n            'import_path': 'chatterbot.logic.MathematicalEvaluation',\n        }\n    ],\n    'storage_adapter': 'chatterbot.storage.DjangoStorageAdapter',\n    'django_app_name': constants.DEFAULT_DJANGO_APP_NAME,\n    'initialize': False\n}\n\nMIDDLEWARE_CLASSES = (\n    'django.contrib.sessions.middleware.SessionMiddleware',\n    'django.middleware.common.CommonMiddleware',\n    'django.contrib.auth.middleware.AuthenticationMiddleware',\n    'django.contrib.auth.middleware.SessionAuthenticationMiddleware',\n    'django.contrib.messages.middleware.MessageMiddleware',\n    'django.middleware.clickjacking.XFrameOptionsMiddleware',\n    'django.middleware.security.SecurityMiddleware',\n)\n\nDATABASES = {\n    'default': {\n        'ENGINE': 'django.db.backends.sqlite3',\n        'NAME': ':memory:',  # Use an in-memory database for faster tests\n    }\n}\n\n# Using the MD5 password hasher improves test performance\nPASSWORD_HASHERS = (\n    'django.contrib.auth.hashers.MD5PasswordHasher',\n)\n\nUSE_TZ = True\n\nLOGGING = {\n    'version': 1,\n    'disable_existing_loggers': False,\n    'handlers': {\n        'console': {\n            'class': 'logging.StreamHandler',\n        },\n    },\n    'loggers': {\n        'django': {\n            'handlers': ['console'],\n            'level': os.getenv('DJANGO_LOG_LEVEL', 'INFO'),\n        },\n    },\n}\n"
  },
  {
    "path": "tests/django_integration/test_statement_integration.py",
    "content": "from datetime import datetime, timezone\nfrom django.test import TestCase\nfrom chatterbot.conversation import Statement as StatementObject\nfrom chatterbot.ext.django_chatterbot.models import Statement as StatementModel\n\n\nclass StatementIntegrationTestCase(TestCase):\n    \"\"\"\n    Test case to make sure that the Django Statement model\n    and ChatterBot Statement object have a common interface.\n    \"\"\"\n\n    def setUp(self):\n        super().setUp()\n\n        now = datetime(2020, 2, 15, 3, 14, 10, 0, timezone.utc)\n\n        self.object = StatementObject(text='_', created_at=now)\n        self.model = StatementModel(text='_', created_at=now)\n\n        # Simulate both statements being saved\n        self.model.save()\n        self.object.id = self.model.id\n\n    def test_text(self):\n        self.assertTrue(hasattr(self.object, 'text'))\n        self.assertTrue(hasattr(self.model, 'text'))\n\n    def test_in_response_to(self):\n        self.assertTrue(hasattr(self.object, 'in_response_to'))\n        self.assertTrue(hasattr(self.model, 'in_response_to'))\n\n    def test_conversation(self):\n        self.assertTrue(hasattr(self.object, 'conversation'))\n        self.assertTrue(hasattr(self.model, 'conversation'))\n\n    def test_tags(self):\n        self.assertTrue(hasattr(self.object, 'tags'))\n        self.assertTrue(hasattr(self.model, 'tags'))\n\n    def test__str__(self):\n        self.assertTrue(hasattr(self.object, '__str__'))\n        self.assertTrue(hasattr(self.model, '__str__'))\n\n        self.assertEqual(str(self.object), str(self.model))\n\n    def test_add_tags(self):\n        self.object.add_tags('a', 'b')\n        self.model.add_tags('a', 'b')\n\n        self.assertIn('a', self.object.get_tags())\n        self.assertIn('a', self.model.get_tags())\n\n    def test_serialize(self):\n        object_data = self.object.serialize()\n        model_data = self.model.serialize()\n\n        self.assertEqual(object_data, model_data)\n"
  },
  {
    "path": "tests/logic/__init__.py",
    "content": ""
  },
  {
    "path": "tests/logic/test_best_match.py",
    "content": "from chatterbot.logic import BestMatch\nfrom chatterbot.conversation import Statement\nfrom tests.base_case import ChatBotTestCase\n\n\nclass BestMatchTestCase(ChatBotTestCase):\n    \"\"\"\n    Unit tests for the BestMatch logic adapter.\n    \"\"\"\n\n    def setUp(self):\n        super().setUp()\n        self.adapter = BestMatch(self.chatbot)\n\n    def test_no_data(self):\n        \"\"\"\n        If there is no data to return, an exception should be raised.\n        \"\"\"\n        statement = self._add_search_text(text='What is your quest?')\n        response = self.adapter.process(statement)\n\n        self.assertEqual(response.text, 'What is your quest?')\n        self.assertEqual(response.confidence, 0)\n\n    def test_no_choices(self):\n        \"\"\"\n        The input should be returned as the closest match if there\n        are no other results to return.\n        \"\"\"\n        self._create_with_search_text(text='Random')\n\n        statement = self._add_search_text(text='What is your quest?')\n        response = self.adapter.process(statement)\n\n        self.assertEqual(response.text, 'Random')\n        self.assertEqual(response.confidence, 0)\n\n    def test_no_known_responses(self):\n        \"\"\"\n        A match can be selected which has no known responses.\n        In this case a random response will be returned, but the confidence\n        should be zero because it is a random choice.\n        \"\"\"\n        from unittest.mock import MagicMock\n\n        self.chatbot.storage.update = MagicMock()\n        self.chatbot.storage.count = MagicMock(return_value=1)\n        self.chatbot.storage.get_random = MagicMock(\n            return_value=Statement(text='Random')\n        )\n\n        match = self.adapter.process(self._add_search_text(text='Blah'))\n\n        self.assertEqual(match.confidence, 0)\n        self.assertEqual(match.text, 'Random')\n\n    def test_match_with_no_response(self):\n        \"\"\"\n        A response to the input should be returned if a response is known.\n        \"\"\"\n        self._create_with_search_text(\n            text='To eat pasta.',\n            in_response_to='What is your quest?'\n        )\n\n        statement = self._add_search_text(text='What is your quest?')\n        response = self.adapter.process(statement)\n\n        self.assertEqual(response.text, 'To eat pasta.')\n        self.assertEqual(response.confidence, 1)\n\n    def test_match_with_response(self):\n        \"\"\"\n        The response to the input should be returned if a response is known.\n        \"\"\"\n        self._create_with_search_text(\n            text='To eat pasta.',\n            in_response_to='What is your quest?'\n        )\n        self._create_with_search_text(\n            text='What is your quest?'\n        )\n\n        statement = self._add_search_text(text='What is your quest?')\n        response = self.adapter.process(statement)\n\n        self.assertEqual(response.text, 'To eat pasta.')\n        self.assertEqual(response.confidence, 1)\n\n    def test_excluded_words(self):\n        \"\"\"\n        Test that the logic adapter cannot return a response containing\n        any of the listed words for exclusion.\n        \"\"\"\n        self._create_with_search_text(\n            text='I like to count.'\n        )\n        self._create_with_search_text(\n            text='Counting is dumb.',\n            in_response_to='I like to count.'\n        )\n        self._create_with_search_text(\n            text='Counting is fun!',\n            in_response_to='I like to count.'\n        )\n\n        self.adapter.excluded_words = ['dumb']\n\n        input_statement = self._add_search_text(text='I like to count.')\n        response = self.adapter.process(input_statement)\n\n        self.assertEqual(response.confidence, 1)\n        self.assertEqual(response.text, 'Counting is fun!')\n\n    def test_low_confidence(self):\n        \"\"\"\n        Test the case that a high confidence response is not known.\n        \"\"\"\n        statement = self._add_search_text(text='Is this a tomato?')\n        match = self.adapter.process(statement)\n\n        self.assertEqual(match.confidence, 0)\n        self.assertEqual(match.text, statement.text)\n\n    def test_low_confidence_options_list(self):\n        \"\"\"\n        Test the case that a high confidence response is not known.\n        \"\"\"\n        self.adapter.default_responses = [\n            Statement(text='No')\n        ]\n\n        statement = self._add_search_text(text='Is this a tomato?')\n        match = self.adapter.process(statement)\n\n        self.assertEqual(match.confidence, 0)\n        self.assertEqual(match.text, 'No')\n\n    def test_text_search_algorithm(self):\n        \"\"\"\n        Test that a close match is found when the text_search algorithm is used.\n        \"\"\"\n        self.adapter = BestMatch(\n            self.chatbot,\n            search_algorithm_name='text_search'\n        )\n\n        self._create_with_search_text(\n            text='I am hungry.'\n        )\n        self._create_with_search_text(\n            text='Okay, what would you like to eat?',\n            in_response_to='I am hungry.'\n        )\n        self._create_with_search_text(\n            text='Can you help me?'\n        )\n        self._create_with_search_text(\n            text='Sure, what seems to be the problem?',\n            in_response_to='Can you help me?'\n        )\n\n        statement = Statement(text='Could you help me?')\n        match = self.adapter.process(statement)\n\n        self.assertEqual(match.confidence, 0.82)\n        self.assertEqual(match.text, 'Sure, what seems to be the problem?')\n"
  },
  {
    "path": "tests/logic/test_data_cache.py",
    "content": "from tests.base_case import ChatBotTestCase\nfrom chatterbot.logic import LogicAdapter\nfrom chatterbot.trainers import ListTrainer\n\n\nclass DummyMutatorLogicAdapter(LogicAdapter):\n    \"\"\"\n    This is a dummy class designed to modify a\n    the resulting statement before it is returned.\n    \"\"\"\n\n    def process(self, statement, additional_response_selection_parameters=None):\n        statement.add_tags('pos_tags:NN')\n        statement.confidence = 1\n        return statement\n\n\nclass DataCachingTests(ChatBotTestCase):\n\n    def setUp(self):\n        super().setUp()\n\n        self.chatbot.logic_adapters = [\n            DummyMutatorLogicAdapter(self.chatbot)\n        ]\n\n        self.trainer = ListTrainer(\n            self.chatbot,\n            show_training_progress=False\n        )\n\n        self.trainer.train([\n            'Hello',\n            'How are you?'\n        ])\n\n    def test_additional_attributes_saved(self):\n        \"\"\"\n        Test that an additional data attribute can be added to the statement\n        and that this attribute is saved.\n        \"\"\"\n        self.chatbot.get_response('Hello', conversation='test')\n        results = list(self.chatbot.storage.filter(\n            text='Hello',\n            in_response_to=None,\n            conversation='test'\n        ))\n\n        self.assertEqual(len(results), 1, msg=f'Results: {[result.serialize() for result in results]}')\n        self.assertIn('pos_tags:NN', results[0].get_tags())\n"
  },
  {
    "path": "tests/logic/test_logic_adapter.py",
    "content": "from tests.base_case import ChatBotTestCase\nfrom chatterbot.logic import LogicAdapter\nfrom chatterbot.conversation import Statement\n\n\nclass LogicAdapterTestCase(ChatBotTestCase):\n    \"\"\"\n    This test case is for the LogicAdapter base class.\n    Although this class is not intended for direct use,\n    this test case ensures that exceptions requiring\n    basic functionality are triggered when needed.\n    \"\"\"\n\n    def setUp(self):\n        super().setUp()\n        self.adapter = LogicAdapter(self.chatbot)\n\n    def test_class_name(self):\n        \"\"\"\n        Test that the logic adapter can return its own class name.\n        \"\"\"\n        self.assertEqual(self.adapter.class_name, 'LogicAdapter')\n\n    def test_can_process(self):\n        \"\"\"\n        This method should return true by default.\n        \"\"\"\n        self.assertTrue(self.adapter.can_process(''))\n\n    def test_process(self):\n        with self.assertRaises(LogicAdapter.AdapterMethodNotImplementedError):\n            self.adapter.process('')\n\n    def test_get_default_response(self):\n        response = self.adapter.get_default_response(Statement(text='...'))\n\n        self.assertEqual(response.text, '...')\n\n    def test_get_default_response_from_options(self):\n        self.adapter.default_responses = [\n            Statement(text='The default')\n        ]\n        response = self.adapter.get_default_response(Statement(text='...'))\n\n        self.assertEqual(response.text, 'The default')\n\n    def test_get_default_response_from_database(self):\n        self._create_with_search_text(text='The default')\n\n        response = self.adapter.get_default_response(Statement(text='...'))\n\n        self.assertEqual(response.text, 'The default')\n"
  },
  {
    "path": "tests/logic/test_mathematical_evaluation.py",
    "content": "from tests.base_case import ChatBotTestCase\nfrom chatterbot.logic import MathematicalEvaluation\nfrom chatterbot.conversation import Statement\n\n\nclass MathematicalEvaluationTests(ChatBotTestCase):\n\n    def setUp(self):\n        super().setUp()\n        self.adapter = MathematicalEvaluation(self.chatbot)\n\n    def test_can_process(self):\n        statement = Statement(text='What is 10 + 10 + 10?')\n        self.assertTrue(self.adapter.can_process(statement))\n\n    def test_can_not_process(self):\n        statement = Statement(text='What is your favorite song?')\n        self.assertFalse(self.adapter.can_process(statement))\n\n    def test_addition_operator(self):\n        statement = Statement(text='What is 100 + 54?')\n        response = self.adapter.process(statement)\n        self.assertEqual(response.text, '100 + 54 = 154')\n        self.assertEqual(response.confidence, 1)\n\n    def test_subtraction_operator(self):\n        statement = Statement(text='What is 100 - 58?')\n        response = self.adapter.process(statement)\n        self.assertEqual(response.text, '100 - 58 = 42')\n        self.assertEqual(response.confidence, 1)\n\n    def test_multiplication_operator(self):\n        statement = Statement(text='What is 100 * 20')\n        response = self.adapter.process(statement)\n        self.assertEqual(response.text, '100 * 20 = 2000')\n        self.assertEqual(response.confidence, 1)\n\n    def test_division_operator(self):\n        statement = Statement(text='What is 100 / 20')\n        response = self.adapter.process(statement)\n\n        self.assertEqual(response.text, '100 / 20 = 5')\n        self.assertEqual(response.confidence, 1)\n\n    def test_exponent_operator(self):\n        statement = Statement(text='What is 2 ^ 10')\n        response = self.adapter.process(statement)\n        self.assertEqual(response.text, '2 ^ 10 = 1024')\n        self.assertEqual(response.confidence, 1)\n\n    def test_parenthesized_multiplication_and_addition(self):\n        statement = Statement(text='What is 100 + ( 1000 * 2 )?')\n        response = self.adapter.process(statement)\n        self.assertEqual(response.text, '100 + ( 1000 * 2 ) = 2100')\n        self.assertEqual(response.confidence, 1)\n\n    def test_parenthesized_with_words(self):\n        statement = Statement(text='What is four plus 100 + ( 100 * 2 )?')\n        response = self.adapter.process(statement)\n        self.assertEqual(response.text, 'four plus 100 + ( 100 * 2 ) = 304')\n        self.assertEqual(response.confidence, 1)\n\n    def test_word_numbers_addition(self):\n        statement = Statement(text='What is one hundred + four hundred?')\n        response = self.adapter.process(statement)\n        self.assertEqual(response.text, 'one hundred + four hundred = 500')\n        self.assertEqual(response.confidence, 1)\n\n    def test_word_division_operator(self):\n        statement = Statement(text='What is 100 divided by 100?')\n        response = self.adapter.process(statement)\n\n        self.assertEqual(response.text, '100 divided by 100 = 1')\n        self.assertEqual(response.confidence, 1)\n\n    def test_large_word_division_operator(self):\n        statement = Statement(text='What is one thousand two hundred four divided by one hundred?')\n        response = self.adapter.process(statement)\n\n        self.assertEqual(response.text, 'one thousand two hundred four divided by one hundred = 12.04')\n\n        self.assertEqual(response.confidence, 1)\n\n    def test_negative_multiplication(self):\n        statement = Statement(text='What is -105 * 5')\n        response = self.adapter.process(statement)\n        self.assertEqual(response.text, '-105 * 5 = -525')\n        self.assertEqual(response.confidence, 1)\n\n    def test_negative_decimal_multiplication(self):\n        statement = Statement(text='What is -100.5 * 20?')\n        response = self.adapter.process(statement)\n        self.assertEqual(response.text, '-100.5 * 20 = -2010.0')\n        self.assertEqual(response.confidence, 1)\n\n    def test_pi_constant(self):\n        statement = Statement(text='What is pi plus one ?')\n        response = self.adapter.process(statement)\n        self.assertEqual(response.text, 'pi plus one = 4.141693')\n        self.assertEqual(response.confidence, 1)\n\n    def test_e_constant(self):\n        statement = Statement(text='What is e plus one ?')\n        response = self.adapter.process(statement)\n        self.assertEqual(response.text, 'e plus one = 3.718281')\n        self.assertEqual(response.confidence, 1)\n\n    def test_log_function(self):\n        statement = Statement(text='What is log 100 ?')\n        response = self.adapter.process(statement)\n        self.assertEqual(response.text, 'log 100 = 2.0')\n        self.assertEqual(response.confidence, 1)\n\n    def test_square_root_function(self):\n        statement = Statement(text='What is the sqrt 144 ?')\n        response = self.adapter.process(statement)\n        self.assertEqual(response.text, 'sqrt 144 = 12.0')\n        self.assertEqual(response.confidence, 1)\n"
  },
  {
    "path": "tests/logic/test_specific_response.py",
    "content": "from tests.base_case import ChatBotTestCase\nfrom chatterbot.logic import SpecificResponseAdapter\nfrom chatterbot.conversation import Statement\nfrom spacy.matcher import Matcher\n\n\nclass SpecificResponseAdapterTestCase(ChatBotTestCase):\n    \"\"\"\n    Test cases for the SpecificResponseAdapter\n    \"\"\"\n\n    def setUp(self):\n        super().setUp()\n        self.adapter = SpecificResponseAdapter(\n            self.chatbot,\n            input_text='Open sesame!',\n            output_text='Your sesame seed hamburger roll is now open.'\n        )\n\n    def test_initialization_with_missing_input_text(self):\n        \"\"\"\"\n        Test that an exception is raised if input_text is missing.\n        \"\"\"\n        with self.assertRaises(self.chatbot.ChatBotException):\n            SpecificResponseAdapter(\n                self.chatbot,\n                output_text='Done!'\n            )\n\n    def test_initialization_with_missing_output_text(self):\n        \"\"\"\"\n        Test that an exception is raised if output_text is missing.\n        \"\"\"\n        with self.assertRaises(self.chatbot.ChatBotException):\n            SpecificResponseAdapter(\n                self.chatbot,\n                input_text='Do something!'\n            )\n\n    def test_exact_match(self):\n        \"\"\"\n        Test the case that an exact match is given.\n        \"\"\"\n        statement = Statement(text='Open sesame!')\n        match = self.adapter.process(statement)\n\n        self.assertEqual(match.confidence, 1)\n        self.assertEqual(match.text, 'Your sesame seed hamburger roll is now open.')\n\n    def test_not_exact_match(self):\n        \"\"\"\n        Test the case that an exact match is not given.\n        \"\"\"\n        statement = Statement(text='Open says me!')\n        match = self.adapter.process(statement)\n\n        self.assertEqual(match.confidence, 0)\n        self.assertEqual(match.text, 'Your sesame seed hamburger roll is now open.')\n\n\nclass SpecificResponseAdapterSpacyTestCase(ChatBotTestCase):\n    \"\"\"\n    Tests specific response adapter with spacy.\n    \"\"\"\n\n    def setUp(self):\n        super().setUp()\n\n        pattern = [\n            {\n                'LOWER': 'open'\n            },\n            {\n                'LOWER': 'sesame'\n            }\n        ]\n\n        self.adapter = SpecificResponseAdapter(\n            self.chatbot,\n            input_text=pattern,\n            matcher=Matcher,\n            output_text='Your sesame seed hamburger roll is now open.',\n            use_patterns=False\n        )\n\n    def test_pattern_match(self):\n        \"\"\"\n        Test the case that a pattern match is given.\n        \"\"\"\n        statement = Statement(text='Open sesame!')\n        match = self.adapter.process(statement)\n\n        self.assertEqual(match.confidence, 1)\n        self.assertEqual(match.text, 'Your sesame seed hamburger roll is now open.')\n\n\nclass SpecificResponseAdapterFunctionResponseTestCase(ChatBotTestCase):\n    \"\"\"\n    Tests specific response adapter using a function that returns a response.\n    \"\"\"\n\n    def setUp(self):\n        super().setUp()\n\n        def response_function():\n            return 'Your sesame seed hamburger roll is now open.'\n\n        self.adapter = SpecificResponseAdapter(\n            self.chatbot,\n            input_text='Open sesame!',\n            output_text=response_function\n        )\n\n    def test_function_response(self):\n        \"\"\"\n        Test the case that a function is given as the output_text.\n        \"\"\"\n        statement = Statement(text='Open sesame!')\n        match = self.adapter.process(statement)\n\n        self.assertEqual(match.confidence, 1)\n        self.assertEqual(match.text, 'Your sesame seed hamburger roll is now open.')\n"
  },
  {
    "path": "tests/logic/test_time.py",
    "content": "from tests.base_case import ChatBotTestCase\nfrom chatterbot.logic import TimeLogicAdapter\nfrom chatterbot.conversation import Statement\n\n\nclass TimeAdapterTests(ChatBotTestCase):\n\n    def setUp(self):\n        super().setUp()\n        self.adapter = TimeLogicAdapter(self.chatbot)\n\n    def test_positive_input(self):\n        statement = Statement(text=\"Do you know what time it is?\")\n        response = self.adapter.process(statement)\n\n        self.assertEqual(response.confidence, 1)\n        self.assertIn(\"The current time is \", response.text)\n\n    def test_negative_input(self):\n        statement = Statement(text=\"What is an example of a pachyderm?\")\n        response = self.adapter.process(statement)\n\n        self.assertEqual(response.confidence, 0)\n        self.assertIn(\"The current time is \", response.text)\n"
  },
  {
    "path": "tests/logic/test_unit_conversion.py",
    "content": "from tests.base_case import ChatBotTestCase\nfrom chatterbot.logic import UnitConversion\nfrom chatterbot.conversation import Statement\n\n\nclass UnitConversionTests(ChatBotTestCase):\n    def setUp(self):\n        super().setUp()\n        self.adapter = UnitConversion(self.chatbot)\n\n    def test_can_process(self):\n        statement = Statement(text='How many inches are in two kilometers?')\n        self.assertTrue(self.adapter.can_process(statement))\n\n    def test_can_process_pattern_x_unit_to_y_unit(self):\n        statement = Statement(text='0 Celsius to fahrenheit')\n        self.assertTrue(self.adapter.can_process(statement))\n\n    def test_can_process_x_unit_is_how_many_y_unit(self):\n        statement = Statement(text='2 TB is how many GB?')\n        self.assertTrue(self.adapter.can_process(statement))\n\n    def test_can_not_process(self):\n        statement = Statement(text='What is love?')\n        self.assertFalse(self.adapter.can_process(statement))\n\n    def test_can_not_convert_inches_to_kilometer(self):\n        statement = Statement(text='How many inches are in blue kilometer?')\n        self.assertFalse(self.adapter.can_process(statement))\n\n    def test_inches_to_kilometers(self):\n        statement = Statement(text='How many inches are in two kilometers?')\n        self.assertTrue(self.adapter.can_process(statement))\n        expected_value = 78740.2\n        response_statement = self.adapter.process(statement)\n        self.assertIsNotNone(response_statement)\n        self.assertEqual(response_statement.confidence, 1)\n        self.assertLessEqual(abs(float(response_statement.text) - expected_value), 0.1)\n\n    def test_inches_to_kilometers_variation_1(self):\n        statement = Statement(text='How many inches in two kilometers?')\n        self.assertTrue(self.adapter.can_process(statement))\n        expected_value = 78740.2\n        response_statement = self.adapter.process(statement)\n        self.assertIsNotNone(response_statement)\n        self.assertEqual(response_statement.confidence, 1)\n        self.assertLessEqual(abs(float(response_statement.text) - expected_value), 0.1)\n\n    def test_inches_to_kilometers_variation_2(self):\n        statement = Statement(text='how many  inches  in two  kilometers ?')\n        self.assertTrue(self.adapter.can_process(statement))\n        expected_value = 78740.2\n        response_statement = self.adapter.process(statement)\n        self.assertIsNotNone(response_statement)\n        self.assertEqual(response_statement.confidence, 1)\n        self.assertLessEqual(abs(float(response_statement.text) - expected_value), 0.1)\n\n    def test_inches_to_kilometers_variation_3(self):\n        statement = Statement(text='how many  inches  in 2  kilometers  ?')\n        self.assertTrue(self.adapter.can_process(statement))\n        expected_value = 78740.2\n        response_statement = self.adapter.process(statement)\n        self.assertIsNotNone(response_statement)\n        self.assertEqual(response_statement.confidence, 1)\n        self.assertLessEqual(abs(float(response_statement.text) - expected_value), 0.1)\n\n    def test_meter_to_kilometer(self):\n        statement = Statement(text='How many meters are in one kilometer?')\n        self.assertTrue(self.adapter.can_process(statement))\n        expected_value = 1000\n        response_statement = self.adapter.process(statement)\n        self.assertIsNotNone(response_statement)\n        self.assertEqual(response_statement.confidence, 1)\n        self.assertLessEqual(abs(float(response_statement.text) - expected_value), 0.1)\n\n    def test_meter_to_kilometer_variation(self):\n        statement = Statement(text='How many meters are in a kilometer?')\n        self.assertTrue(self.adapter.can_process(statement))\n        expected_value = 1000\n        response_statement = self.adapter.process(statement)\n        self.assertIsNotNone(response_statement)\n        self.assertEqual(response_statement.confidence, 1)\n        self.assertLessEqual(abs(float(response_statement.text) - expected_value), 0.1)\n\n    def test_temperature_celsius_to_fahrenheit(self):\n        statement = Statement(text='How many fahrenheit are in 0 celsius ?')\n        self.assertTrue(self.adapter.can_process(statement))\n        expected_value = 32\n        response_statement = self.adapter.process(statement)\n        self.assertIsNotNone(response_statement)\n        self.assertEqual(response_statement.confidence, 1)\n        self.assertLessEqual(abs(float(response_statement.text) - expected_value), 0.1)\n\n    def test_negative_temperature_celsius_to_fahrenheit(self):\n        statement = Statement(text='How many fahrenheit are in -0.2 celsius ?')\n        self.assertTrue(self.adapter.can_process(statement))\n        expected_value = 31.64\n        response_statement = self.adapter.process(statement)\n        self.assertIsNotNone(response_statement)\n        self.assertEqual(response_statement.confidence, 1)\n        self.assertLessEqual(abs(float(response_statement.text) - expected_value), 0.1)\n\n    def test_time_two_hours_to_seconds(self):\n        statement = Statement(text='How many seconds are in two hours?')\n        self.assertTrue(self.adapter.can_process(statement))\n        expected_value = 7200\n        response_statement = self.adapter.process(statement)\n        self.assertIsNotNone(response_statement)\n        self.assertEqual(response_statement.confidence, 1)\n        self.assertLessEqual(abs(float(response_statement.text) - expected_value), 0.1)\n\n    def test_pattern_x_unit_to_y_unit(self):\n        statement = Statement(text='-11 Celsius to kelvin')\n        self.assertTrue(self.adapter.can_process(statement))\n        expected_value = 262.15\n        response_statement = self.adapter.process(statement)\n        self.assertIsNotNone(response_statement)\n        self.assertEqual(response_statement.confidence, 1)\n        self.assertLessEqual(abs(float(response_statement.text) - expected_value), 0.1)\n\n    def test_pattern_x_unit_is_how_many_y_unit(self):\n        statement = Statement(text='2 TB is how many GB?')\n        self.assertTrue(self.adapter.can_process(statement))\n        expected_value = 2000\n        response_statement = self.adapter.process(statement)\n        self.assertIsNotNone(response_statement)\n        self.assertEqual(response_statement.confidence, 1)\n        self.assertLessEqual(abs(float(response_statement.text) - expected_value), 0.1)\n"
  },
  {
    "path": "tests/storage/__init__.py",
    "content": ""
  },
  {
    "path": "tests/storage/test_mongo_adapter.py",
    "content": "from unittest import TestCase\nfrom chatterbot.storage import MongoDatabaseAdapter\nfrom chatterbot.conversation import Statement\n\n\nclass MongoAdapterTestCase(TestCase):\n\n    @classmethod\n    def setUpClass(cls):\n        \"\"\"\n        Instantiate the adapter before any tests in the test case run.\n        \"\"\"\n        from pymongo.errors import ServerSelectionTimeoutError\n        from pymongo import MongoClient\n\n        cls.has_mongo_connection = False\n\n        cls.database_uri = 'mongodb://localhost:27017/chatterbot_test_database'\n\n        try:\n            client = MongoClient(\n                serverSelectionTimeoutMS=0.1\n            )\n            client.server_info()\n\n            cls.adapter = MongoDatabaseAdapter(\n                database_uri=cls.database_uri,\n                raise_on_missing_search_text=False\n            )\n\n            cls.has_mongo_connection = True\n\n            client.close()\n\n        except ServerSelectionTimeoutError:\n            pass\n\n    @classmethod\n    def tearDownClass(cls):\n        \"\"\"\n        Close the MongoDB client connection after all tests have run.\n        \"\"\"\n        if cls.has_mongo_connection:\n            cls.adapter.client.close()\n\n    def setUp(self):\n        \"\"\"\n        Skip these tests if a mongo client is not running.\n        \"\"\"\n        if not self.has_mongo_connection:\n            self.skipTest('Unable to connect to mongo database.')\n\n    def tearDown(self):\n        \"\"\"\n        Remove the test database.\n        \"\"\"\n        self.adapter.drop()\n\n\nclass MongoDatabaseAdapterTestCase(MongoAdapterTestCase):\n\n    def test_count_returns_zero(self):\n        \"\"\"\n        The count method should return a value of 0\n        when nothing has been saved to the database.\n        \"\"\"\n        self.assertEqual(self.adapter.count(), 0)\n\n    def test_count_returns_value(self):\n        \"\"\"\n        The count method should return a value of 1\n        when one item has been saved to the database.\n        \"\"\"\n        self.adapter.create(text=\"Test statement\")\n        self.assertEqual(self.adapter.count(), 1)\n\n    def test_mongodb_client_kwargs_parameter(self):\n        \"\"\"\n        Test that the adapter accepts mongodb_client_kwargs parameter.\n        This enables SSL/TLS connections to services like Amazon DocumentDB.\n        \"\"\"\n        adapter = MongoDatabaseAdapter(\n            database_uri=self.database_uri,\n            mongodb_client_kwargs={\n                'serverSelectionTimeoutMS': 100,\n                'connectTimeoutMS': 100\n            },\n            raise_on_missing_search_text=False\n        )\n\n        # Verify the adapter was created successfully\n        self.assertIsNotNone(adapter)\n        self.assertIsNotNone(adapter.client)\n\n        # Clean up\n        adapter.close()\n\n    def test_filter_text_statement_not_found(self):\n        \"\"\"\n        Test that None is returned by the find method\n        when a matching statement is not found.\n        \"\"\"\n        results = list(self.adapter.filter(text='Non-existent'))\n        self.assertEqual(len(results), 0)\n\n    def test_filter_text_statement_found(self):\n        \"\"\"\n        Test that a matching statement is returned\n        when it exists in the database.\n        \"\"\"\n        self.adapter.create(text='New statement')\n        results = list(self.adapter.filter(text='New statement'))\n\n        self.assertEqual(len(results), 1)\n        self.assertEqual(results[0].text, 'New statement')\n\n    def test_update_adds_new_statement(self):\n        self.adapter.create(text='New statement')\n\n        results = list(self.adapter.filter(text='New statement'))\n\n        self.assertEqual(len(results), 1)\n        self.assertEqual(results[0].text, 'New statement')\n\n    def test_update_modifies_existing_statement(self):\n        statement = Statement(text=\"New statement\")\n        self.adapter.update(statement)\n\n        # Check the initial values\n        results = list(self.adapter.filter(text=statement.text))\n\n        self.assertEqual(len(results), 1)\n        self.assertEqual(results[0].in_response_to, None)\n\n        # Update the statement value\n        statement.in_response_to = \"New response\"\n\n        self.adapter.update(statement)\n\n        # Check that the values have changed\n        results = list(self.adapter.filter(text=statement.text))\n\n        self.assertEqual(len(results), 1)\n        self.assertEqual(results[0].in_response_to, \"New response\")\n\n    def test_get_random_returns_statement(self):\n        text = \"New statement\"\n        self.adapter.create(text=text)\n        random_statement = self.adapter.get_random()\n\n        self.assertEqual(random_statement.text, text)\n\n    def test_get_random_no_data(self):\n        from chatterbot.storage import StorageAdapter\n\n        with self.assertRaises(StorageAdapter.EmptyDatabaseException):\n            self.adapter.get_random()\n\n    def test_mongo_to_object(self):\n        self.adapter.create(text='Hello', in_response_to='Hi')\n        statement_data = self.adapter.statements.find_one({'text': 'Hello'})\n\n        obj = self.adapter.mongo_to_object(statement_data)\n\n        self.assertEqual(type(obj), Statement)\n        self.assertEqual(obj.text, 'Hello')\n        self.assertEqual(obj.in_response_to, 'Hi')\n        self.assertEqual(obj.id, statement_data['_id'])\n\n    def test_remove(self):\n        text = \"Sometimes you have to run before you can walk.\"\n        self.adapter.create(text=text)\n        self.adapter.remove(text)\n        results = list(self.adapter.filter(text=text))\n\n        self.assertEqual(results, [])\n\n    def test_remove_response(self):\n        text = \"Sometimes you have to run before you can walk.\"\n        self.adapter.create(text='', in_response_to=text)\n        self.adapter.remove(text)\n        results = list(self.adapter.filter(text=text))\n\n        self.assertEqual(results, [])\n\n\nclass MongoAdapterFilterTestCase(MongoAdapterTestCase):\n\n    def test_filter_text_no_matches(self):\n        self.adapter.create(\n            text='Testing...',\n            in_response_to='Why are you counting?'\n        )\n        results = list(self.adapter.filter(text='Howdy'))\n\n        self.assertEqual(len(results), 0)\n\n    def test_filter_in_response_to_no_matches(self):\n        self.adapter.create(\n            text='Testing...',\n            in_response_to='Why are you counting?'\n        )\n\n        results = list(self.adapter.filter(in_response_to='Maybe'))\n        self.assertEqual(len(results), 0)\n\n    def test_filter_equal_results(self):\n        statement1 = Statement(\n            text=\"Testing...\",\n            in_response_to=[]\n        )\n        statement2 = Statement(\n            text=\"Testing one, two, three.\",\n            in_response_to=[]\n        )\n        self.adapter.update(statement1)\n        self.adapter.update(statement2)\n\n        results = list(self.adapter.filter(in_response_to=[]))\n\n        results_text = [\n            result.text for result in results\n        ]\n\n        self.assertEqual(len(results), 2)\n        self.assertIn(statement1.text, results_text)\n        self.assertIn(statement2.text, results_text)\n\n    def test_filter_no_parameters(self):\n        \"\"\"\n        If no parameters are passed to the filter,\n        then all statements should be returned.\n        \"\"\"\n        self.adapter.create(text=\"Testing...\")\n        self.adapter.create(text=\"Testing one, two, three.\")\n\n        results = list(self.adapter.filter())\n\n        self.assertEqual(len(results), 2)\n\n    def test_filter_in_response_to(self):\n        self.adapter.create(text=\"A\", in_response_to=\"Yes\")\n        self.adapter.create(text=\"B\", in_response_to=\"No\")\n\n        results = list(self.adapter.filter(\n            in_response_to=\"Yes\"\n        ))\n\n        # Get the first response\n        response = results[0]\n\n        self.assertEqual(len(results), 1)\n        self.assertEqual(response.in_response_to, \"Yes\")\n\n    def test_filter_by_tag(self):\n        self.adapter.create(text=\"Hello!\", tags=[\"greeting\", \"salutation\"])\n        self.adapter.create(text=\"Hi everyone!\", tags=[\"greeting\", \"exclamation\"])\n        self.adapter.create(text=\"The air contains Oxygen.\", tags=[\"fact\"])\n\n        results = list(self.adapter.filter(tags=[\"greeting\"]))\n\n        results_text_list = [statement.text for statement in results]\n\n        self.assertEqual(len(results_text_list), 2)\n        self.assertIn(\"Hello!\", results_text_list)\n        self.assertIn(\"Hi everyone!\", results_text_list)\n\n    def test_filter_by_tags(self):\n        self.adapter.create(text=\"Hello!\", tags=[\"greeting\", \"salutation\"])\n        self.adapter.create(text=\"Hi everyone!\", tags=[\"greeting\", \"exclamation\"])\n        self.adapter.create(text=\"The air contains Oxygen.\", tags=[\"fact\"])\n\n        results = list(self.adapter.filter(\n            tags=[\"exclamation\", \"fact\"]\n        ))\n\n        results_text_list = [statement.text for statement in results]\n\n        self.assertEqual(len(results_text_list), 2)\n        self.assertIn(\"Hi everyone!\", results_text_list)\n        self.assertIn(\"The air contains Oxygen.\", results_text_list)\n\n    def test_filter_page_size(self):\n        self.adapter.create(text='A')\n        self.adapter.create(text='B')\n        self.adapter.create(text='C')\n\n        results = self.adapter.filter(page_size=2)\n\n        results_text_list = [statement.text for statement in results]\n\n        self.assertEqual(len(results_text_list), 3)\n        self.assertIn('A', results_text_list)\n        self.assertIn('B', results_text_list)\n        self.assertIn('C', results_text_list)\n\n    def test_exclude_text(self):\n        self.adapter.create(text='Hello!')\n        self.adapter.create(text='Hi everyone!')\n\n        results = list(self.adapter.filter(\n            exclude_text=[\n                'Hello!'\n            ]\n        ))\n\n        self.assertEqual(len(results), 1)\n        self.assertEqual(results[0].text, 'Hi everyone!')\n\n    def test_exclude_text_words(self):\n        self.adapter.create(text='This is a good example.')\n        self.adapter.create(text='This is a bad example.')\n        self.adapter.create(text='This is a worse example.')\n\n        results = list(self.adapter.filter(\n            exclude_text_words=[\n                'bad', 'worse'\n            ]\n        ))\n\n        self.assertEqual(len(results), 1)\n        self.assertEqual(results[0].text, 'This is a good example.')\n\n    def test_persona_not_startswith(self):\n        self.adapter.create(text='Hello!', persona='bot:tester')\n        self.adapter.create(text='Hi everyone!', persona='user:person')\n\n        results = list(self.adapter.filter(\n            persona_not_startswith='bot:'\n        ))\n\n        self.assertEqual(len(results), 1)\n        self.assertEqual(results[0].text, 'Hi everyone!')\n\n    def test_search_text_contains(self):\n        self.adapter.create(text='Hello!', search_text='hello exclamation')\n        self.adapter.create(text='Hi everyone!', search_text='hi everyone')\n\n        results = list(self.adapter.filter(\n            search_text_contains='everyone'\n        ))\n\n        self.assertEqual(len(results), 1)\n        self.assertEqual(results[0].text, 'Hi everyone!')\n\n    def test_search_text_contains_multiple_matches(self):\n        self.adapter.create(text='Hello!', search_text='hello exclamation')\n        self.adapter.create(text='Hi everyone!', search_text='hi everyone')\n\n        results = list(self.adapter.filter(\n            search_text_contains='hello everyone'\n        ))\n\n        self.assertEqual(len(results), 2)\n\n\nclass MongoOrderingTestCase(MongoAdapterTestCase):\n    \"\"\"\n    Test cases for the ordering of sets of statements.\n    \"\"\"\n\n    def test_order_by_text(self):\n        statement_a = Statement(text='A is the first letter of the alphabet.')\n        statement_b = Statement(text='B is the second letter of the alphabet.')\n\n        self.adapter.update(statement_b)\n        self.adapter.update(statement_a)\n\n        results = list(self.adapter.filter(order_by=['text']))\n\n        self.assertEqual(len(results), 2)\n        self.assertEqual(statement_a.text, results[0].text)\n        self.assertEqual(statement_b.text, results[1].text)\n\n    def test_order_by_created_at(self):\n        from datetime import datetime, timedelta\n\n        today = datetime.now()\n        yesterday = datetime.now() - timedelta(days=1)\n\n        statement_a = Statement(\n            text='A is the first letter of the alphabet.',\n            created_at=today\n        )\n        statement_b = Statement(\n            text='B is the second letter of the alphabet.',\n            created_at=yesterday\n        )\n\n        self.adapter.update(statement_b)\n        self.adapter.update(statement_a)\n\n        results = list(self.adapter.filter(order_by=['created_at']))\n\n        self.assertEqual(len(results), 2)\n        self.assertEqual(statement_a.text, results[0].text)\n        self.assertEqual(statement_b.text, results[1].text)\n\n\nclass StorageAdapterCreateTestCase(MongoAdapterTestCase):\n    \"\"\"\n    Tests for the create function of the storage adapter.\n    \"\"\"\n\n    def test_create_text(self):\n        self.adapter.create(text='testing')\n\n        results = list(self.adapter.filter())\n\n        self.assertEqual(len(results), 1)\n        self.assertEqual(results[0].text, 'testing')\n\n    def test_create_search_text(self):\n        self.adapter.create(\n            text='testing',\n            search_text='test'\n        )\n\n        results = list(self.adapter.filter())\n\n        self.assertEqual(len(results), 1)\n        self.assertEqual(results[0].search_text, 'test')\n\n    def test_create_search_in_response_to(self):\n        self.adapter.create(\n            text='testing',\n            search_in_response_to='test'\n        )\n\n        results = list(self.adapter.filter())\n\n        self.assertEqual(len(results), 1)\n        self.assertEqual(results[0].search_in_response_to, 'test')\n\n    def test_create_tags(self):\n        self.adapter.create(text='testing', tags=['a', 'b'])\n\n        results = list(self.adapter.filter())\n\n        self.assertEqual(len(results), 1)\n        self.assertIn('a', results[0].get_tags())\n        self.assertIn('b', results[0].get_tags())\n\n    def test_create_duplicate_tags(self):\n        \"\"\"\n        The storage adapter should not create a statement with tags\n        that are duplicates.\n        \"\"\"\n        self.adapter.create(text='testing', tags=['ab', 'ab'])\n\n        results = list(self.adapter.filter())\n\n        self.assertEqual(len(results), 1)\n        self.assertEqual(len(results[0].get_tags()), 1)\n        self.assertEqual(results[0].get_tags(), ['ab'])\n\n    def test_create_many_text(self):\n        self.adapter.create_many([\n            Statement(text='A'),\n            Statement(text='B')\n        ])\n\n        results = list(self.adapter.filter())\n\n        self.assertEqual(len(results), 2)\n        self.assertEqual(results[0].text, 'A')\n        self.assertEqual(results[1].text, 'B')\n\n    def test_create_many_search_text(self):\n        self.adapter.create_many([\n            Statement(text='A', search_text='a'),\n            Statement(text='B', search_text='b')\n        ])\n\n        results = list(self.adapter.filter())\n\n        self.assertEqual(len(results), 2)\n        self.assertEqual(results[0].search_text, 'a')\n        self.assertEqual(results[1].search_text, 'b')\n\n    def test_create_many_search_in_response_to(self):\n        # `search_text` must be present or `search_in_response_to` will be generated based on the `in_response_to` field\n        self.adapter.create_many([\n            Statement(text='A', search_text='1', search_in_response_to='a'),\n            Statement(text='B', search_text='2', search_in_response_to='b')\n        ])\n\n        results = list(self.adapter.filter())\n\n        self.assertEqual(len(results), 2)\n        self.assertEqual(results[0].search_in_response_to, 'a')\n        self.assertEqual(results[1].search_in_response_to, 'b')\n\n    def test_create_many_tags(self):\n        self.adapter.create_many([\n            Statement(text='A', tags=['first', 'letter']),\n            Statement(text='B', tags=['second', 'letter'])\n        ])\n        results = list(self.adapter.filter())\n\n        self.assertEqual(len(results), 2)\n        self.assertIn('letter', results[0].get_tags())\n        self.assertIn('letter', results[1].get_tags())\n        self.assertIn('first', results[0].get_tags())\n        self.assertIn('second', results[1].get_tags())\n\n    def test_create_many_duplicate_tags(self):\n        \"\"\"\n        The storage adapter should not create a statement with tags\n        that are duplicates.\n        \"\"\"\n        self.adapter.create_many([\n            Statement(text='testing', tags=['ab', 'ab'])\n        ])\n\n        results = list(self.adapter.filter())\n\n        self.assertEqual(len(results), 1)\n        self.assertEqual(len(results[0].get_tags()), 1)\n        self.assertEqual(results[0].get_tags(), ['ab'])\n\n\nclass StorageAdapterUpdateTestCase(MongoAdapterTestCase):\n    \"\"\"\n    Tests for the update function of the storage adapter.\n    \"\"\"\n\n    def test_update_adds_tags(self):\n        statement = self.adapter.create(text='Testing')\n        statement.add_tags('a', 'b')\n        self.adapter.update(statement)\n\n        statements = list(self.adapter.filter())\n\n        self.assertEqual(len(statements), 1)\n        self.assertIn('a', statements[0].get_tags())\n        self.assertIn('b', statements[0].get_tags())\n\n    def test_update_duplicate_tags(self):\n        \"\"\"\n        The storage adapter should not update a statement with tags\n        that are duplicates.\n        \"\"\"\n        statement = self.adapter.create(text='Testing', tags=['ab'])\n        statement.add_tags('ab')\n        self.adapter.update(statement)\n\n        statements = list(self.adapter.filter())\n\n        self.assertEqual(len(statements), 1)\n        self.assertEqual(len(statements[0].get_tags()), 1)\n        self.assertEqual(statements[0].get_tags(), ['ab'])\n"
  },
  {
    "path": "tests/storage/test_redis_adapter.py",
    "content": "import sys\nimport unittest\nfrom unittest import TestCase\nfrom chatterbot.conversation import Statement\nfrom chatterbot.storage.redis import RedisVectorStorageAdapter\n\n\n@unittest.skipIf(\n    sys.version_info < (3, 10),\n    'The Redis adapter requires Python 3.10+'\n)\nclass RedisStorageAdapterTestCase(TestCase):\n\n    @classmethod\n    def setUpClass(cls):\n        \"\"\"\n        Instantiate the adapter before any tests in the test case run.\n        \"\"\"\n        from huggingface_hub import snapshot_download\n\n        # Download the model from Hugging Face before running the tests\n        snapshot_download(repo_id='sentence-transformers/all-mpnet-base-v2', repo_type='model')\n\n        cls.adapter = RedisVectorStorageAdapter(\n            database_uri='redis://localhost:6379/0'\n        )\n\n    def tearDown(self):\n        \"\"\"\n        Drop the tables in the database after each test is run.\n        \"\"\"\n        self.adapter.drop()\n\n\nclass RedisStorageAdapterTests(RedisStorageAdapterTestCase):\n\n    def test_count_returns_zero(self):\n        \"\"\"\n        The count method should return a value of 0\n        when nothing has been saved to the database.\n        \"\"\"\n        self.assertEqual(self.adapter.count(), 0)\n\n    def test_count_returns_value(self):\n        \"\"\"\n        The count method should return a value of 1\n        when one item has been saved to the database.\n        \"\"\"\n        self.adapter.create(text='Test statement')\n        self.assertEqual(self.adapter.count(), 1)\n\n    def test_filter_text_statement_not_found(self):\n        \"\"\"\n        Test that an empty list is returned by the filter method\n        when a matching statement is not found.\n        \"\"\"\n        results = list(self.adapter.filter(text='Non-existent'))\n        self.assertEqual(len(results), 0)\n\n    def test_filter_text_statement_found(self):\n        \"\"\"\n        Test that a matching statement is returned\n        when it exists in the database.\n        \"\"\"\n        text = 'New statement'\n\n        self.adapter.create(text=text)\n        results = self.adapter.filter(text=text)\n\n        self.assertEqual(len(results), 1)\n        self.assertEqual(results[0].text, text)\n\n    def test_update_adds_new_statement(self):\n        statement = Statement(text='New statement')\n        self.adapter.update(statement)\n\n        results = list(self.adapter.filter(text='New statement'))\n        self.assertEqual(len(results), 1)\n        self.assertEqual(results[0].text, statement.text)\n\n    def test_update_modifies_existing_statement(self):\n        statement = self.adapter.create(text='New statement')\n\n        # Check the initial values\n        results = list(self.adapter.filter(text=statement.text))\n\n        self.assertEqual(len(results), 1)\n        self.assertEqual(results[0].in_response_to, None)\n\n        # Update the statement value\n        statement.in_response_to = 'New response'\n        self.adapter.update(statement)\n\n        # Check that the values have changed\n        results = list(self.adapter.filter(text=statement.text))\n\n        self.assertEqual(len(results), 1, msg=[(\n            result.id, result.text, result.in_response_to) for result in results\n        ])\n        self.assertEqual(results[0].in_response_to, 'New response')\n\n    def test_get_random_returns_statement(self):\n        self.adapter.create(text='New statement')\n\n        random_statement = self.adapter.get_random()\n        self.assertEqual(random_statement.text, 'New statement')\n\n    def test_get_random_no_data(self):\n        from chatterbot.storage import StorageAdapter\n\n        with self.assertRaises(StorageAdapter.EmptyDatabaseException):\n            self.adapter.get_random()\n\n    def test_remove(self):\n        text = 'Sometimes you have to run before you can walk.'\n        statement = self.adapter.create(text=text)\n\n        self.adapter.remove(statement)\n\n        results = self.adapter.filter(text=text)\n\n        self.assertEqual(list(results), [])\n\n\nclass RedisStorageAdapterFilterTests(RedisStorageAdapterTestCase):\n\n    def test_filter_text_no_matches(self):\n        self.adapter.create(\n            text='Testing...',\n            in_response_to='Why are you counting?'\n        )\n        results = list(self.adapter.filter(text='Howdy'))\n\n        # No results will be returned because the text not an close enough match\n        self.assertEqual(len(results), 0)\n\n    def test_filter_in_response_to_no_matches(self):\n        self.adapter.create(\n            text='Testing...',\n            in_response_to='Why are you counting?'\n        )\n\n        results = list(self.adapter.filter(in_response_to='Maybe'))\n\n        self.assertEqual(len(results), 0)\n\n    def test_filter_equal_results(self):\n        statement1 = Statement(\n            text='Testing...',\n            in_response_to=None\n        )\n        statement2 = Statement(\n            text='Testing one, two, three.',\n            in_response_to=None\n        )\n        self.adapter.update(statement1)\n        self.adapter.update(statement2)\n\n        results = list(self.adapter.filter(in_response_to=None))\n\n        results_text = [\n            result.text for result in results\n        ]\n\n        self.assertEqual(len(results), 2)\n        self.assertIn(statement1.text, results_text)\n        self.assertIn(statement2.text, results_text)\n\n    def test_filter_no_parameters(self):\n        \"\"\"\n        If no parameters are passed to the filter,\n        then all statements should be returned.\n        \"\"\"\n        self.adapter.create(text='Testing...')\n        self.adapter.create(text='Testing one, two, three.')\n\n        results = list(self.adapter.filter())\n\n        self.assertEqual(len(results), 2)\n\n    def test_filter_by_tag(self):\n        self.adapter.create(text='Hello!', tags=['greeting', 'salutation'])\n        self.adapter.create(text='Hi everyone!', tags=['greeting', 'exclamation'])\n        self.adapter.create(text='The air contains Oxygen.', tags=['fact'])\n\n        results = self.adapter.filter(tags=['greeting'])\n\n        results_text_list = [statement.text for statement in results]\n\n        self.assertEqual(len(results_text_list), 2)\n        self.assertIn('Hello!', results_text_list)\n        self.assertIn('Hi everyone!', results_text_list)\n\n    def test_filter_by_tags(self):\n        self.adapter.create(text='Hello!', tags=['greeting', 'salutation'])\n        self.adapter.create(text='Hi everyone!', tags=['greeting', 'exclamation'])\n        self.adapter.create(text='The air contains Oxygen.', tags=['fact'])\n\n        results = self.adapter.filter(\n            tags=['exclamation', 'fact']\n        )\n\n        results_text_list = [statement.text for statement in results]\n\n        self.assertEqual(len(results_text_list), 2)\n        self.assertIn('Hi everyone!', results_text_list)\n        self.assertIn('The air contains Oxygen.', results_text_list)\n\n    def test_filter_page_size(self):\n        self.adapter.create(text='A')\n        self.adapter.create(text='B')\n        self.adapter.create(text='C')\n\n        results = self.adapter.filter(page_size=2)\n\n        results_text_list = [statement.text for statement in results]\n\n        # Check that page_size limit is respected\n        self.assertEqual(len(results_text_list), 2)\n        # Verify all returned results are from the created set (order may vary)\n        for text in results_text_list:\n            self.assertIn(text, ['A', 'B', 'C'])\n\n    def test_exclude_text(self):\n        self.adapter.create(text='Hello!')\n        self.adapter.create(text='Hi everyone!')\n\n        results = list(self.adapter.filter(\n            exclude_text=[\n                'Hello!'\n            ]\n        ))\n\n        self.assertEqual(len(results), 1)\n        self.assertEqual(results[0].text, 'Hi everyone!')\n\n    def test_exclude_text_words(self):\n        self.adapter.create(text='This is a good example.')\n        self.adapter.create(text='This is a bad example.')\n        self.adapter.create(text='This is a worse example.')\n\n        results = list(self.adapter.filter(\n            exclude_text_words=[\n                'bad', 'worse'\n            ]\n        ))\n\n        self.assertEqual(len(results), 1)\n        self.assertEqual(results[0].text, 'This is a good example.')\n\n    def test_persona_not_startswith(self):\n        self.adapter.create(text='Hello!', persona='bot:tester')\n        self.adapter.create(text='Hi everyone!', persona='user:person')\n\n        results = list(self.adapter.filter(\n            persona_not_startswith='bot:'\n        ))\n\n        self.assertEqual(len(results), 1)\n        self.assertEqual(results[0].text, 'Hi everyone!')\n\n    def test_search_in_response_to_contains(self):\n        self.adapter.create(text='A', in_response_to='hello all')\n        self.adapter.create(text='B', in_response_to='hi everyone')\n\n        results = list(self.adapter.filter(\n            search_in_response_to_contains='everyone',\n            page_size=1\n        ))\n\n        self.assertEqual(len(results), 1)\n        self.assertEqual(results[0].text, 'B')\n        self.assertEqual(results[0].in_response_to, 'hi everyone')\n\n    def test_search_in_response_to_contains_multiple_matches(self):\n        self.adapter.create(text='A', in_response_to='hello all')\n        self.adapter.create(text='B', in_response_to='hi everyone')\n\n        results = list(self.adapter.filter(\n            search_in_response_to_contains='hello everyone'\n        ))\n\n        self.assertEqual(len(results), 2)\n\n\nclass RedisOrderingTests(RedisStorageAdapterTestCase):\n    \"\"\"\n    Test cases for the ordering of sets of statements.\n    \"\"\"\n\n    def test_order_by_text(self):\n        statement_a = Statement(text='A is the first letter of the alphabet.')\n        statement_b = Statement(text='B is the second letter of the alphabet.')\n\n        self.adapter.update(statement_b)\n        self.adapter.update(statement_a)\n\n        results = list(self.adapter.filter(order_by=['text']))\n\n        self.assertEqual(len(results), 2)\n        self.assertEqual(statement_a.text, results[0].text)\n        self.assertEqual(statement_b.text, results[1].text)\n\n    def test_order_by_created_at(self):\n        from datetime import datetime, timedelta\n\n        today = datetime.now()\n        yesterday = datetime.now() - timedelta(days=1)\n\n        statement_a = Statement(\n            text='A is the first letter of the alphabet.',\n            created_at=yesterday\n        )\n        statement_b = Statement(\n            text='B is the second letter of the alphabet.',\n            created_at=today\n        )\n\n        self.adapter.update(statement_b)\n        self.adapter.update(statement_a)\n\n        results = list(self.adapter.filter(order_by=['created_at']))\n\n        self.assertEqual(len(results), 2)\n        self.assertEqual(statement_a.text, results[0].text)\n        self.assertEqual(statement_b.text, results[1].text)\n\n\nclass StorageAdapterCreateTests(RedisStorageAdapterTestCase):\n    \"\"\"\n    Tests for the create function of the storage adapter.\n    \"\"\"\n\n    def test_create_text(self):\n        self.adapter.create(text='testing')\n\n        results = list(self.adapter.filter())\n\n        self.assertEqual(len(results), 1)\n        self.assertEqual(results[0].text, 'testing')\n\n    def test_create_tags(self):\n        self.adapter.create(text='testing', tags=['a', 'b'])\n\n        results = list(self.adapter.filter())\n\n        self.assertEqual(len(results), 1)\n        self.assertIn('a', results[0].get_tags())\n        self.assertIn('b', results[0].get_tags())\n\n    def test_create_duplicate_tags(self):\n        \"\"\"\n        The storage adapter should not create a statement with tags\n        that are duplicates.\n        \"\"\"\n        self.adapter.create(text='testing', tags=['ab', 'ab'])\n\n        results = list(self.adapter.filter())\n\n        self.assertEqual(len(results), 1)\n        self.assertEqual(len(results[0].get_tags()), 1, msg=results[0].get_tags())\n        self.assertEqual(results[0].get_tags(), ['ab'])\n\n    def test_create_many_text(self):\n        self.adapter.create_many([\n            Statement(text='A'),\n            Statement(text='B')\n        ])\n\n        results = list(self.adapter.filter())\n\n        self.assertEqual(len(results), 2)\n        results_text = [r.text for r in results]\n        self.assertIn('A', results_text)\n        self.assertIn('B', results_text)\n\n    def test_create_many_tags(self):\n        self.adapter.create_many([\n            Statement(text='A', tags=['first', 'letter']),\n            Statement(text='B', tags=['second', 'letter'])\n        ])\n        results = list(self.adapter.filter())\n\n        self.assertEqual(len(results), 2)\n\n        # Find which result is which (order may vary)\n        result_a = next((r for r in results if r.text == 'A'), None)\n        result_b = next((r for r in results if r.text == 'B'), None)\n\n        self.assertIsNotNone(result_a, \"Statement with text 'A' not found\")\n        self.assertIsNotNone(result_b, \"Statement with text 'B' not found\")\n\n        self.assertIn('letter', result_a.get_tags())\n        self.assertIn('first', result_a.get_tags())\n        self.assertIn('letter', result_b.get_tags())\n        self.assertIn('second', result_b.get_tags())\n\n    def test_create_many_duplicate_tags(self):\n        \"\"\"\n        The storage adapter should not create a statement with tags\n        that are duplicates.\n        \"\"\"\n        self.adapter.create_many([\n            Statement(text='testing', tags=['ab', 'ab'])\n        ])\n\n        results = list(self.adapter.filter())\n\n        self.assertEqual(len(results), 1)\n        self.assertEqual(len(results[0].get_tags()), 1)\n        self.assertEqual(results[0].get_tags(), ['ab'])\n\n\nclass StorageAdapterUpdateTests(RedisStorageAdapterTestCase):\n    \"\"\"\n    Tests for the update function of the storage adapter.\n    \"\"\"\n\n    def test_update_adds_tags(self):\n        statement = self.adapter.create(text='Testing')\n        statement.add_tags('a', 'b')\n        self.adapter.update(statement)\n\n        statements = list(self.adapter.filter())\n\n        self.assertEqual(len(statements), 1)\n        self.assertIn('a', statements[0].get_tags())\n        self.assertIn('b', statements[0].get_tags())\n\n    def test_update_duplicate_tags(self):\n        \"\"\"\n        The storage adapter should not update a statement with tags\n        that are duplicates.\n        \"\"\"\n        statement = self.adapter.create(text='Testing', tags=['ab'])\n        statement.add_tags('ab')\n        self.adapter.update(statement)\n\n        statements = list(self.adapter.filter())\n\n        self.assertEqual(len(statements), 1)\n        self.assertEqual(len(statements[0].get_tags()), 1)\n        self.assertEqual(statements[0].get_tags(), ['ab'])\n"
  },
  {
    "path": "tests/storage/test_sql_adapter.py",
    "content": "from unittest import TestCase\nfrom chatterbot.conversation import Statement\nfrom chatterbot.storage.sql_storage import SQLStorageAdapter\n\n\nclass SQLStorageAdapterTestCase(TestCase):\n\n    @classmethod\n    def setUpClass(cls):\n        \"\"\"\n        Instantiate the adapter before any tests in the test case run.\n        \"\"\"\n        cls.adapter = SQLStorageAdapter(database_uri=None, raise_on_missing_search_text=False)\n\n    @classmethod\n    def tearDownClass(cls):\n        \"\"\"\n        Close the adapter connection after all tests are run.\n        \"\"\"\n        cls.adapter.close()\n\n    def tearDown(self):\n        \"\"\"\n        Drop the tables in the database after each test is run.\n        \"\"\"\n        self.adapter.drop()\n\n\nclass SQLStorageAdapterTests(SQLStorageAdapterTestCase):\n\n    def test_set_database_uri_none(self):\n        adapter = SQLStorageAdapter(database_uri=None)\n        self.assertEqual(adapter.database_uri, 'sqlite://')\n        adapter.close()\n\n    def test_set_database_uri(self):\n        adapter = SQLStorageAdapter(database_uri='sqlite:///db.sqlite3')\n        self.assertEqual(adapter.database_uri, 'sqlite:///db.sqlite3')\n        adapter.close()\n\n    def test_count_returns_zero(self):\n        \"\"\"\n        The count method should return a value of 0\n        when nothing has been saved to the database.\n        \"\"\"\n        self.assertEqual(self.adapter.count(), 0)\n\n    def test_count_returns_value(self):\n        \"\"\"\n        The count method should return a value of 1\n        when one item has been saved to the database.\n        \"\"\"\n        self.adapter.create(text=\"Test statement\")\n        self.assertEqual(self.adapter.count(), 1)\n\n    def test_filter_text_statement_not_found(self):\n        \"\"\"\n        Test that None is returned by the find method\n        when a matching statement is not found.\n        \"\"\"\n        results = list(self.adapter.filter(text=\"Non-existent\"))\n        self.assertEqual(len(results), 0)\n\n    def test_filter_text_statement_found(self):\n        \"\"\"\n        Test that a matching statement is returned\n        when it exists in the database.\n        \"\"\"\n        text = \"New statement\"\n        self.adapter.create(text=text)\n        results = list(self.adapter.filter(text=text))\n\n        self.assertEqual(len(results), 1)\n        self.assertEqual(results[0].text, text)\n\n    def test_update_adds_new_statement(self):\n        statement = Statement(text=\"New statement\")\n        self.adapter.update(statement)\n\n        results = list(self.adapter.filter(text=\"New statement\"))\n        self.assertEqual(len(results), 1)\n        self.assertEqual(results[0].text, statement.text)\n\n    def test_update_modifies_existing_statement(self):\n        statement = Statement(text=\"New statement\")\n        self.adapter.update(statement)\n\n        # Check the initial values\n        results = list(self.adapter.filter(text=statement.text))\n\n        self.assertEqual(len(results), 1)\n        self.assertEqual(results[0].in_response_to, None)\n\n        # Update the statement value\n        statement.in_response_to = \"New response\"\n        self.adapter.update(statement)\n\n        # Check that the values have changed\n        results = list(self.adapter.filter(text=statement.text))\n\n        self.assertEqual(len(results), 1)\n        self.assertEqual(results[0].in_response_to, \"New response\")\n\n    def test_get_random_returns_statement(self):\n        self.adapter.create(text=\"New statement\")\n\n        random_statement = self.adapter.get_random()\n        self.assertEqual(random_statement.text, \"New statement\")\n\n    def test_get_random_no_data(self):\n        from chatterbot.storage import StorageAdapter\n\n        with self.assertRaises(StorageAdapter.EmptyDatabaseException):\n            self.adapter.get_random()\n\n    def test_remove(self):\n        text = \"Sometimes you have to run before you can walk.\"\n        self.adapter.create(text=text)\n        self.adapter.remove(text)\n        results = self.adapter.filter(text=text)\n\n        self.assertEqual(list(results), [])\n\n\nclass SQLStorageAdapterFilterTests(SQLStorageAdapterTestCase):\n\n    def test_filter_text_no_matches(self):\n        self.adapter.create(\n            text='Testing...',\n            in_response_to='Why are you counting?'\n        )\n        results = list(self.adapter.filter(text=\"Howdy\"))\n\n        self.assertEqual(len(results), 0)\n\n    def test_filter_in_response_to_no_matches(self):\n        self.adapter.create(\n            text='Testing...',\n            in_response_to='Why are you counting?'\n        )\n\n        results = list(self.adapter.filter(in_response_to=\"Maybe\"))\n\n        self.assertEqual(len(results), 0)\n\n    def test_filter_equal_results(self):\n        statement1 = Statement(\n            text=\"Testing...\",\n            in_response_to=None\n        )\n        statement2 = Statement(\n            text=\"Testing one, two, three.\",\n            in_response_to=None\n        )\n        self.adapter.update(statement1)\n        self.adapter.update(statement2)\n\n        results = list(self.adapter.filter(in_response_to=None))\n\n        results_text = [\n            result.text for result in results\n        ]\n\n        self.assertEqual(len(results), 2)\n        self.assertIn(statement1.text, results_text)\n        self.assertIn(statement2.text, results_text)\n\n    def test_filter_no_parameters(self):\n        \"\"\"\n        If no parameters are passed to the filter,\n        then all statements should be returned.\n        \"\"\"\n        self.adapter.create(text=\"Testing...\")\n        self.adapter.create(text=\"Testing one, two, three.\")\n\n        results = list(self.adapter.filter())\n\n        self.assertEqual(len(results), 2)\n\n    def test_filter_by_tag(self):\n        self.adapter.create(text=\"Hello!\", tags=[\"greeting\", \"salutation\"])\n        self.adapter.create(text=\"Hi everyone!\", tags=[\"greeting\", \"exclamation\"])\n        self.adapter.create(text=\"The air contains Oxygen.\", tags=[\"fact\"])\n\n        results = self.adapter.filter(tags=[\"greeting\"])\n\n        results_text_list = [statement.text for statement in results]\n\n        self.assertEqual(len(results_text_list), 2)\n        self.assertIn(\"Hello!\", results_text_list)\n        self.assertIn(\"Hi everyone!\", results_text_list)\n\n    def test_filter_by_tags(self):\n        self.adapter.create(text=\"Hello!\", tags=[\"greeting\", \"salutation\"])\n        self.adapter.create(text=\"Hi everyone!\", tags=[\"greeting\", \"exclamation\"])\n        self.adapter.create(text=\"The air contains Oxygen.\", tags=[\"fact\"])\n\n        results = self.adapter.filter(\n            tags=[\"exclamation\", \"fact\"]\n        )\n\n        results_text_list = [statement.text for statement in results]\n\n        self.assertEqual(len(results_text_list), 2)\n        self.assertIn(\"Hi everyone!\", results_text_list)\n        self.assertIn(\"The air contains Oxygen.\", results_text_list)\n\n    def test_filter_page_size(self):\n        self.adapter.create(text='A')\n        self.adapter.create(text='B')\n        self.adapter.create(text='C')\n\n        results = self.adapter.filter(page_size=2)\n\n        results_text_list = [statement.text for statement in results]\n\n        self.assertEqual(len(results_text_list), 3)\n        self.assertIn('A', results_text_list)\n        self.assertIn('B', results_text_list)\n        self.assertIn('C', results_text_list)\n\n    def test_exclude_text(self):\n        self.adapter.create(text='Hello!')\n        self.adapter.create(text='Hi everyone!')\n\n        results = list(self.adapter.filter(\n            exclude_text=[\n                'Hello!'\n            ]\n        ))\n\n        self.assertEqual(len(results), 1)\n        self.assertEqual(results[0].text, 'Hi everyone!')\n\n    def test_exclude_text_words(self):\n        self.adapter.create(text='This is a good example.')\n        self.adapter.create(text='This is a bad example.')\n        self.adapter.create(text='This is a worse example.')\n\n        results = list(self.adapter.filter(\n            exclude_text_words=[\n                'bad', 'worse'\n            ]\n        ))\n\n        self.assertEqual(len(results), 1)\n        self.assertEqual(results[0].text, 'This is a good example.')\n\n    def test_persona_not_startswith(self):\n        self.adapter.create(text='Hello!', persona='bot:tester')\n        self.adapter.create(text='Hi everyone!', persona='user:person')\n\n        results = list(self.adapter.filter(\n            persona_not_startswith='bot:'\n        ))\n\n        self.assertEqual(len(results), 1)\n        self.assertEqual(results[0].text, 'Hi everyone!')\n\n    def test_search_text_contains(self):\n        self.adapter.create(text='Hello!', search_text='hello exclamation')\n        self.adapter.create(text='Hi everyone!', search_text='hi everyone')\n\n        results = list(self.adapter.filter(\n            search_text_contains='everyone'\n        ))\n\n        self.assertEqual(len(results), 1)\n        self.assertEqual(results[0].text, 'Hi everyone!')\n\n    def test_search_text_contains_multiple_matches(self):\n        self.adapter.create(text='Hello!', search_text='hello exclamation')\n        self.adapter.create(text='Hi everyone!', search_text='hi everyone')\n\n        results = list(self.adapter.filter(\n            search_text_contains='hello everyone'\n        ))\n\n        self.assertEqual(len(results), 2)\n\n\nclass SQLOrderingTests(SQLStorageAdapterTestCase):\n    \"\"\"\n    Test cases for the ordering of sets of statements.\n    \"\"\"\n\n    def test_order_by_text(self):\n        statement_a = Statement(text='A is the first letter of the alphabet.')\n        statement_b = Statement(text='B is the second letter of the alphabet.')\n\n        self.adapter.update(statement_b)\n        self.adapter.update(statement_a)\n\n        results = list(self.adapter.filter(order_by=['text']))\n\n        self.assertEqual(len(results), 2)\n        self.assertEqual(statement_a.text, results[0].text)\n        self.assertEqual(statement_b.text, results[1].text)\n\n    def test_order_by_created_at(self):\n        from datetime import datetime, timedelta\n\n        today = datetime.now()\n        yesterday = datetime.now() - timedelta(days=1)\n\n        statement_a = Statement(\n            text='A is the first letter of the alphabet.',\n            created_at=yesterday\n        )\n        statement_b = Statement(\n            text='B is the second letter of the alphabet.',\n            created_at=today\n        )\n\n        self.adapter.update(statement_b)\n        self.adapter.update(statement_a)\n\n        results = list(self.adapter.filter(order_by=['created_at']))\n\n        self.assertEqual(len(results), 2)\n        self.assertEqual(statement_a.text, results[0].text)\n        self.assertEqual(statement_b.text, results[1].text)\n\n\nclass StorageAdapterCreateTests(SQLStorageAdapterTestCase):\n    \"\"\"\n    Tests for the create function of the storage adapter.\n    \"\"\"\n\n    def test_create_text(self):\n        self.adapter.create(text='testing')\n\n        results = list(self.adapter.filter())\n\n        self.assertEqual(len(results), 1)\n        self.assertEqual(results[0].text, 'testing')\n\n    def test_create_search_text(self):\n        self.adapter.create(\n            text='testing',\n            search_text='test'\n        )\n\n        results = list(self.adapter.filter())\n\n        self.assertEqual(len(results), 1)\n        self.assertEqual(results[0].search_text, 'test')\n\n    def test_create_search_in_response_to(self):\n        self.adapter.create(\n            text='testing',\n            search_in_response_to='test'\n        )\n\n        results = list(self.adapter.filter())\n\n        self.assertEqual(len(results), 1)\n        self.assertEqual(results[0].search_in_response_to, 'test')\n\n    def test_create_tags(self):\n        self.adapter.create(text='testing', tags=['a', 'b'])\n\n        results = list(self.adapter.filter())\n\n        self.assertEqual(len(results), 1)\n        self.assertIn('a', results[0].get_tags())\n        self.assertIn('b', results[0].get_tags())\n\n    def test_create_duplicate_tags(self):\n        \"\"\"\n        The storage adapter should not create a statement with tags\n        that are duplicates.\n        \"\"\"\n        self.adapter.create(text='testing', tags=['ab', 'ab'])\n\n        results = list(self.adapter.filter())\n\n        self.assertEqual(len(results), 1)\n        self.assertEqual(len(results[0].get_tags()), 1)\n        self.assertEqual(results[0].get_tags(), ['ab'])\n\n    def test_create_many_text(self):\n        self.adapter.create_many([\n            Statement(text='A'),\n            Statement(text='B')\n        ])\n\n        results = list(self.adapter.filter())\n\n        self.assertEqual(len(results), 2)\n        self.assertEqual(results[0].text, 'A')\n        self.assertEqual(results[1].text, 'B')\n\n    def test_create_many_search_text(self):\n        self.adapter.create_many([\n            Statement(text='A', search_text='a'),\n            Statement(text='B', search_text='b')\n        ])\n\n        results = list(self.adapter.filter())\n\n        self.assertEqual(len(results), 2)\n        self.assertEqual(results[0].search_text, 'a')\n        self.assertEqual(results[1].search_text, 'b')\n\n    def test_create_many_search_in_response_to(self):\n        # `search_text` must be present or `search_in_response_to` will be generated based on the `in_response_to` field\n        self.adapter.create_many([\n            Statement(text='A', search_text='1', search_in_response_to='a'),\n            Statement(text='B', search_text='2', search_in_response_to='b')\n        ])\n\n        results = list(self.adapter.filter())\n\n        self.assertEqual(len(results), 2)\n        self.assertEqual(results[0].search_in_response_to, 'a')\n        self.assertEqual(results[1].search_in_response_to, 'b')\n\n    def test_create_many_tags(self):\n        self.adapter.create_many([\n            Statement(text='A', tags=['first', 'letter']),\n            Statement(text='B', tags=['second', 'letter'])\n        ])\n        results = list(self.adapter.filter())\n\n        self.assertEqual(len(results), 2)\n        self.assertIn('letter', results[0].get_tags())\n        self.assertIn('letter', results[1].get_tags())\n        self.assertIn('first', results[0].get_tags())\n        self.assertIn('second', results[1].get_tags())\n\n    def test_create_many_duplicate_tags(self):\n        \"\"\"\n        The storage adapter should not create a statement with tags\n        that are duplicates.\n        \"\"\"\n        self.adapter.create_many([\n            Statement(text='testing', tags=['ab', 'ab'])\n        ])\n\n        results = list(self.adapter.filter())\n\n        self.assertEqual(len(results), 1)\n        self.assertEqual(len(results[0].get_tags()), 1)\n        self.assertEqual(results[0].get_tags(), ['ab'])\n\n\nclass StorageAdapterUpdateTests(SQLStorageAdapterTestCase):\n    \"\"\"\n    Tests for the update function of the storage adapter.\n    \"\"\"\n\n    def test_update_adds_tags(self):\n        statement = self.adapter.create(text='Testing')\n        statement.add_tags('a', 'b')\n        self.adapter.update(statement)\n\n        statements = list(self.adapter.filter())\n\n        self.assertEqual(len(statements), 1)\n        self.assertIn('a', statements[0].get_tags())\n        self.assertIn('b', statements[0].get_tags())\n\n    def test_update_duplicate_tags(self):\n        \"\"\"\n        The storage adapter should not update a statement with tags\n        that are duplicates.\n        \"\"\"\n        statement = self.adapter.create(text='Testing', tags=['ab'])\n        statement.add_tags('ab')\n        self.adapter.update(statement)\n\n        statements = list(self.adapter.filter())\n\n        self.assertEqual(len(statements), 1)\n        self.assertEqual(len(statements[0].get_tags()), 1)\n        self.assertEqual(statements[0].get_tags(), ['ab'])\n"
  },
  {
    "path": "tests/storage/test_storage_adapter.py",
    "content": "from unittest import TestCase\nfrom chatterbot.storage import StorageAdapter\n\n\nclass StorageAdapterTestCase(TestCase):\n    \"\"\"\n    This test case is for the StorageAdapter base class.\n    Although this class is not intended for direct use,\n    this test case ensures that exceptions requiring\n    basic functionality are triggered when needed.\n    \"\"\"\n\n    @classmethod\n    def setUpClass(cls):\n        cls.adapter = StorageAdapter()\n\n    def test_count(self):\n        with self.assertRaises(StorageAdapter.AdapterMethodNotImplementedError):\n            self.adapter.count()\n\n    def test_filter(self):\n        with self.assertRaises(StorageAdapter.AdapterMethodNotImplementedError):\n            self.adapter.filter()\n\n    def test_remove(self):\n        with self.assertRaises(StorageAdapter.AdapterMethodNotImplementedError):\n            self.adapter.remove('')\n\n    def test_create(self):\n        with self.assertRaises(StorageAdapter.AdapterMethodNotImplementedError):\n            self.adapter.create()\n\n    def test_create_many(self):\n        with self.assertRaises(StorageAdapter.AdapterMethodNotImplementedError):\n            self.adapter.create_many([])\n\n    def test_update(self):\n        with self.assertRaises(StorageAdapter.AdapterMethodNotImplementedError):\n            self.adapter.update('')\n\n    def test_get_random(self):\n        with self.assertRaises(StorageAdapter.AdapterMethodNotImplementedError):\n            self.adapter.get_random()\n\n    def test_drop(self):\n        with self.assertRaises(StorageAdapter.AdapterMethodNotImplementedError):\n            self.adapter.drop()\n\n    def test_get_model_invalid(self):\n        with self.assertRaises(AttributeError):\n            self.adapter.get_model('invalid')\n\n    def test_get_object_invalid(self):\n        with self.assertRaises(AttributeError):\n            self.adapter.get_object('invalid')\n"
  },
  {
    "path": "tests/test_adapter_validation.py",
    "content": "from chatterbot import ChatBot\nfrom chatterbot.adapters import Adapter\nfrom tests.base_case import ChatBotTestCase\n\n\nclass AdapterValidationTests(ChatBotTestCase):\n\n    def test_invalid_storage_adapter(self):\n        kwargs = self.get_kwargs()\n        kwargs['storage_adapter'] = 'chatterbot.logic.LogicAdapter'\n        with self.assertRaises(Adapter.InvalidAdapterTypeException):\n            self.chatbot = ChatBot('Test Bot', **kwargs)\n\n    def test_valid_storage_adapter(self):\n        kwargs = self.get_kwargs()\n        kwargs['storage_adapter'] = 'chatterbot.storage.SQLStorageAdapter'\n        try:\n            self.chatbot = ChatBot('Test Bot', **kwargs)\n        except Adapter.InvalidAdapterTypeException:\n            self.fail('Test raised InvalidAdapterException unexpectedly!')\n\n    def test_invalid_logic_adapter(self):\n        kwargs = self.get_kwargs()\n        kwargs['logic_adapters'] = ['chatterbot.storage.StorageAdapter']\n        with self.assertRaises(Adapter.InvalidAdapterTypeException):\n            self.chatbot = ChatBot('Test Bot', **kwargs)\n\n    def test_valid_logic_adapter(self):\n        kwargs = self.get_kwargs()\n        kwargs['logic_adapters'] = ['chatterbot.logic.BestMatch']\n        try:\n            self.chatbot = ChatBot('Test Bot', **kwargs)\n        except Adapter.InvalidAdapterTypeException:\n            self.fail('Test raised InvalidAdapterException unexpectedly!')\n\n    def test_valid_adapter_dictionary(self):\n        kwargs = self.get_kwargs()\n        kwargs['storage_adapter'] = {\n            'import_path': 'chatterbot.storage.SQLStorageAdapter'\n        }\n        try:\n            self.chatbot = ChatBot('Test Bot', **kwargs)\n        except Adapter.InvalidAdapterTypeException:\n            self.fail('Test raised InvalidAdapterException unexpectedly!')\n\n    def test_invalid_adapter_dictionary(self):\n        kwargs = self.get_kwargs()\n        kwargs['storage_adapter'] = {\n            'import_path': 'chatterbot.logic.BestMatch'\n        }\n        with self.assertRaises(Adapter.InvalidAdapterTypeException):\n            self.chatbot = ChatBot('Test Bot', **kwargs)\n"
  },
  {
    "path": "tests/test_benchmarks.py",
    "content": "\"\"\"\nThese tests are designed to test execution time for\nvarious chat bot configurations to help prevent\nperformance based regressions when changes are made.\n\"\"\"\n\nimport logging\nfrom unittest import skip\nfrom warnings import warn\nfrom random import choice\nfrom tests.base_case import ChatBotSQLTestCase, ChatBotMongoTestCase\nfrom chatterbot.trainers import ListTrainer, ChatterBotCorpusTrainer, UbuntuCorpusTrainer\nfrom chatterbot.logic import BestMatch\nfrom chatterbot import comparisons, response_selection, utils\n\n\nlogging.getLogger('pymongo').setLevel(logging.WARNING)\n\n\nWORDBANK = (\n    'the', 'mellifluous', 'sound', 'of', 'a', 'spring', 'evening',\n    'breaks', 'the', 'heart', 'string', 'by', 'calling', 'out', 'to',\n    'David', 'who', 'looks', 'on', 'at', 'the', 'world', 'blankly',\n    'who', 'could', 'tell', 'that', 'there', 'is', 'no', 'instrument',\n    'softly', 'strumming', 'toward', 'the', 'melody', 'called', 'silence',\n)\n\n\n# Generate a list of random sentences\nSTATEMENT_LIST = [\n    '{:s} {:s} {:s} {:s} {:s} {:s} {:s} {:s} {:s} {:s}'.format(\n        *[choice(WORDBANK) for __ in range(0, 10)]\n    ) for _ in range(0, 10)\n]\n\n\ndef get_list_trainer(chatbot):\n    return ListTrainer(\n        chatbot,\n        show_training_progress=False\n    )\n\n\ndef get_chatterbot_corpus_trainer(chatbot):\n    return ChatterBotCorpusTrainer(\n        chatbot,\n        show_training_progress=False\n    )\n\n\ndef get_ubuntu_corpus_trainer(chatbot):\n    return UbuntuCorpusTrainer(\n        chatbot,\n        show_training_progress=False\n    )\n\n\nclass BenchmarkingMixin(object):\n\n    def assert_response_duration_is_less_than(self, maximum_duration, strict=False):\n        \"\"\"\n        Assert that the response time did not exceed the maximum allowed amount.\n\n        :param strict: If set to true, test will fail if the maximum duration is exceeded.\n        \"\"\"\n        from sys import stdout\n\n        duration = utils.get_response_time(self.chatbot)\n\n        stdout.write('\\nBENCHMARK: Duration was %f seconds\\n' % duration)\n\n        failure_message = (\n            '{duration} was greater than the maximum allowed '\n            'response time of {maximum_duration}'.format(\n                duration=duration,\n                maximum_duration=maximum_duration\n            )\n        )\n\n        if strict and duration > maximum_duration:\n            raise AssertionError(failure_message)\n        elif duration > maximum_duration:\n            warn(failure_message)\n\n\nclass SqlBenchmarkingTests(BenchmarkingMixin, ChatBotSQLTestCase):\n    \"\"\"\n    Benchmarking tests for SQL storage.\n    \"\"\"\n\n    def get_kwargs(self):\n        kwargs = super().get_kwargs()\n        kwargs['storage_adapter'] = 'chatterbot.storage.SQLStorageAdapter'\n        return kwargs\n\n    def test_levenshtein_distance_comparisons(self):\n        \"\"\"\n        Test the levenshtein distance comparison algorithm.\n        \"\"\"\n        self.chatbot.logic_adapters[0] = BestMatch(\n            self.chatbot,\n            statement_comparison_function=comparisons.LevenshteinDistance,\n            response_selection_method=response_selection.get_first_response\n        )\n\n        trainer = get_list_trainer(self.chatbot)\n        trainer.train(STATEMENT_LIST)\n\n        self.assert_response_duration_is_less_than(1)\n\n    def test_spacy_similarity_comparisons(self):\n        \"\"\"\n        Test the spacy similarity comparison algorithm.\n        \"\"\"\n        self.chatbot.logic_adapters[0] = BestMatch(\n            self.chatbot,\n            statement_comparison_function=comparisons.SpacySimilarity,\n            response_selection_method=response_selection.get_first_response\n        )\n\n        trainer = get_list_trainer(self.chatbot)\n        trainer.train(STATEMENT_LIST)\n\n        self.assert_response_duration_is_less_than(3)\n\n    def test_get_response_after_chatterbot_corpus_training(self):\n        \"\"\"\n        Test response time after training with the ChatterBot corpus.\n        \"\"\"\n        trainer = get_chatterbot_corpus_trainer(self.chatbot)\n        trainer.train('chatterbot.corpus')\n\n        self.assert_response_duration_is_less_than(3)\n\n    @skip('Test marked as skipped due to execution time.')\n    def test_get_response_after_ubuntu_corpus_training(self):\n        \"\"\"\n        Test response time after training with the Ubuntu corpus.\n        \"\"\"\n        trainer = get_ubuntu_corpus_trainer(self.chatbot)\n        trainer.train(limit=50)\n\n        self.assert_response_duration_is_less_than(6)\n\n\nclass MongoBenchmarkingTests(BenchmarkingMixin, ChatBotMongoTestCase):\n    \"\"\"\n    Benchmarking tests for Mongo DB storage.\n    \"\"\"\n\n    def get_kwargs(self):\n        kwargs = super().get_kwargs()\n        kwargs['storage_adapter'] = 'chatterbot.storage.MongoDatabaseAdapter'\n        return kwargs\n\n    def test_levenshtein_distance_comparisons(self):\n        \"\"\"\n        Test the levenshtein distance comparison algorithm.\n        \"\"\"\n        self.chatbot.logic_adapters[0] = BestMatch(\n            self.chatbot,\n            statement_comparison_function=comparisons.LevenshteinDistance,\n            response_selection_method=response_selection.get_first_response\n        )\n\n        trainer = get_list_trainer(self.chatbot)\n        trainer.train(STATEMENT_LIST)\n\n        self.assert_response_duration_is_less_than(1)\n\n    def test_spacy_similarity_comparisons(self):\n        \"\"\"\n        Test the spacy similarity comparison algorithm.\n        \"\"\"\n        self.chatbot.logic_adapters[0] = BestMatch(\n            self.chatbot,\n            statement_comparison_function=comparisons.SpacySimilarity,\n            response_selection_method=response_selection.get_first_response\n        )\n\n        trainer = get_list_trainer(self.chatbot)\n        trainer.train(STATEMENT_LIST)\n\n        self.assert_response_duration_is_less_than(3)\n\n    def test_get_response_after_chatterbot_corpus_training(self):\n        \"\"\"\n        Test response time after training with the ChatterBot corpus.\n        \"\"\"\n        trainer = get_chatterbot_corpus_trainer(self.chatbot)\n        trainer.train('chatterbot.corpus')\n\n        self.assert_response_duration_is_less_than(3)\n\n    @skip('Test marked as skipped due to execution time.')\n    def test_get_response_after_ubuntu_corpus_training(self):\n        \"\"\"\n        Test response time after training with the Ubuntu corpus.\n        \"\"\"\n        trainer = get_ubuntu_corpus_trainer(self.chatbot)\n        trainer.train()\n\n        self.assert_response_duration_is_less_than(6)\n"
  },
  {
    "path": "tests/test_chatbot.py",
    "content": "from tests.base_case import ChatBotTestCase\nfrom chatterbot import ChatBot\nfrom chatterbot.logic import LogicAdapter\nfrom chatterbot.conversation import Statement\nfrom chatterbot import languages\n\n\nclass ChatBotInitializationTestCase(ChatBotTestCase):\n    \"\"\"\n    Test the initialization of the ChatBot class.\n    \"\"\"\n\n    def test_initialization_with_unmapped_spacy_model(self):\n        \"\"\"\n        Test that the chatbot raises an exception if a corresponding spaCy model\n        does not exist for a language.\n        \"\"\"\n        with self.assertRaises(KeyError) as exc:\n            ChatBot(\n                'Test Bot',\n                tagger_language=languages.LAT\n            )\n\n            self.assertEqual(\n                str(exc.exception),\n                'A corresponding spacy model for \"Latin\" could not be found.'\n            )\n\n    def test_initialization_with_missing_spacy_model(self):\n        \"\"\"\n        Test that the chatbot raises an exception if a spaCy model\n        has not been installed for the specified language.\n        \"\"\"\n        with self.assertRaises(ChatBot.ChatBotException) as exc:\n            ChatBot(\n                'Test Bot',\n                tagger_language=languages.NOR\n            )\n            self.assertIn(\n                'model for \"Norwegian\" language is missing',\n                str(exc.exception),\n            )\n            self.assertIn(\n                'spacy download nb_core_news_sm',\n                str(exc.exception),\n            )\n\n\nclass ChatterBotResponseTestCase(ChatBotTestCase):\n\n    def test_conversation_values_persisted_to_response(self):\n        response = self.chatbot.get_response('Hello', persist_values_to_response={\n            'conversation': 'test 1'\n        })\n        self.assertEqual(response.conversation, 'test 1')\n\n    def test_tag_values_persisted_to_response(self):\n        response = self.chatbot.get_response('Hello', persist_values_to_response={\n            'tags': [\n                'tag 1',\n                'tag 2'\n            ]\n        })\n        self.assertEqual(len(response.tags), 2)\n        self.assertIn('tag 1', response.get_tags())\n        self.assertIn('tag 2', response.get_tags())\n\n    def test_in_response_to_provided(self):\n        \"\"\"\n        Test that the process of looking up the previous response\n        in the conversation is ignored if a previous response is provided.\n        \"\"\"\n        self.chatbot.get_response(\n            text='Hello',\n            in_response_to='Unique previous response.'\n        )\n        statement = self.chatbot.storage.filter(\n            text='Hello',\n            in_response_to='Unique previous response.'\n        )\n        self.assertIsNotNone(statement)\n\n    def test_no_statements_known(self):\n        \"\"\"\n        If there are no statements in the database, then the\n        user's input is the only thing that can be returned.\n        \"\"\"\n        statement_text = 'How are you?'\n        response = self.chatbot.get_response(statement_text)\n        results = list(self.chatbot.storage.filter(text=statement_text))\n\n        self.assertEqual(response.text, statement_text)\n        self.assertEqual(response.confidence, 0)\n\n        # Two results will exist, one is generated by the bot\n        self.assertIsLength(results, 2)\n        self.assertEqual(results[0].text, statement_text)\n\n    def test_one_statement_known_no_response(self):\n        \"\"\"\n        Test the case where a single statement is known, but\n        it is not in response to any other statement.\n        \"\"\"\n        self._create_with_search_text(text='Hello', in_response_to=None)\n\n        response = self.chatbot.get_response('Hi')\n\n        self.assertEqual(response.confidence, 0)\n        self.assertEqual(response.text, 'Hello')\n\n    def test_one_statement_one_response_known(self):\n        \"\"\"\n        Test the case that one response is known and there is a response\n        entry for it in the database.\n        \"\"\"\n        self._create_with_search_text(text='Hello', in_response_to='Hi')\n\n        response = self.chatbot.get_response('Hi')\n\n        self.assertEqual(response.confidence, 1)\n        self.assertEqual(response.text, 'Hello')\n\n    def test_two_statements_one_response_known(self):\n        \"\"\"\n        Test the case that one response is known and there is a response\n        entry for it in the database.\n        \"\"\"\n        self._create_with_search_text(text='Hi', in_response_to=None)\n        self._create_with_search_text(text='Hello', in_response_to='Hi')\n\n        response = self.chatbot.get_response('Hi')\n\n        self.assertEqual(response.confidence, 1)\n        self.assertEqual(response.text, 'Hello')\n\n    def test_three_statements_two_responses_known(self):\n        \"\"\"\n        Test the case that one response is known and there is a response\n        entry for it in the database.\n        \"\"\"\n        self._create_with_search_text(text='Hi', in_response_to=None, conversation='test')\n        self._create_with_search_text(text='Hello', in_response_to='Hi', conversation='test')\n        self._create_with_search_text(text='How are you?', in_response_to='Hello', conversation='test')\n\n        first_response = self.chatbot.get_response('Hi', conversation='test')\n        second_response = self.chatbot.get_response('How are you?', conversation='test')\n\n        self.assertEqual(first_response.confidence, 1)\n        self.assertEqual(first_response.text, 'Hello')\n        self.assertEqual(second_response.confidence, 1)\n        self.assertEqual(second_response.text, 'Hi')\n\n    def test_four_statements_three_responses_known(self):\n        self._create_with_search_text(text='Hi', in_response_to=None, conversation='test')\n        self._create_with_search_text(text='Hello', in_response_to='Hi', conversation='test')\n        self._create_with_search_text(text='How are you?', in_response_to='Hello', conversation='test')\n        self._create_with_search_text(text='I am well.', in_response_to='How are you?', conversation='test')\n\n        first_response = self.chatbot.get_response('Hi', conversation='test')\n        second_response = self.chatbot.get_response('How are you?', conversation='test')\n\n        self.assertEqual(first_response.confidence, 1)\n        self.assertEqual(first_response.text, 'Hello')\n        self.assertEqual(second_response.confidence, 1)\n        self.assertEqual(second_response.text, 'I am well.')\n\n    def test_second_response_unknown(self):\n        self._create_with_search_text(text='Hi', in_response_to=None)\n        self._create_with_search_text(text='Hello', in_response_to='Hi')\n\n        first_response = self.chatbot.get_response(\n            text='Hi',\n            conversation='test'\n        )\n        second_response = self.chatbot.get_response(\n            text='How are you?',\n            conversation='test'\n        )\n\n        results = list(self.chatbot.storage.filter(text='How are you?'))\n\n        self.assertEqual(first_response.confidence, 1)\n        self.assertEqual(first_response.text, 'Hello')\n        self.assertEqual(first_response.in_response_to, 'Hi')\n\n        self.assertEqual(second_response.confidence, 0)\n        self.assertEqual(second_response.in_response_to, 'How are you?')\n\n        # Make sure that the previous response was saved to the database\n        self.assertIsLength(results, 1)\n        self.assertEqual(results[0].in_response_to, 'Hello')\n\n    def test_statement_added_to_conversation(self):\n        \"\"\"\n        An input statement should be added to the recent response list.\n        \"\"\"\n        statement = Statement(text='Wow!', conversation='test')\n        response = self.chatbot.get_response(statement)\n\n        self.assertEqual(statement.text, response.text)\n        self.assertEqual(response.conversation, 'test')\n\n    def test_get_response_additional_response_selection_parameters(self):\n        self._create_many_with_search_text([\n            Statement('A', conversation='test_1'),\n            Statement('B', conversation='test_1', in_response_to='A'),\n            Statement('A', conversation='test_2'),\n            Statement('C', conversation='test_2', in_response_to='A'),\n        ])\n\n        statement = Statement(text='A', conversation='test_3')\n        response = self.chatbot.get_response(\n            statement,\n            additional_response_selection_parameters={\n                'conversation': 'test_2'\n            }\n        )\n\n        self.assertEqual(response.text, 'C')\n        self.assertEqual(response.conversation, 'test_3')\n\n    def test_get_response_unicode(self):\n        \"\"\"\n        Test the case that a unicode string is passed in.\n        \"\"\"\n        response = self.chatbot.get_response(u'سلام')\n        self.assertGreater(len(response.text), 0)\n\n    def test_get_response_emoji(self):\n        \"\"\"\n        Test the case that the input string contains an emoji.\n        \"\"\"\n        response = self.chatbot.get_response(u'💩 ')\n        self.assertGreater(len(response.text), 0)\n\n    def test_get_response_non_whitespace(self):\n        \"\"\"\n        Test the case that a non-whitespace C1 control string is passed in.\n        \"\"\"\n        response = self.chatbot.get_response(u'')\n        self.assertGreater(len(response.text), 0)\n\n    def test_get_response_two_byte_characters(self):\n        \"\"\"\n        Test the case that a string containing two-byte characters is passed in.\n        \"\"\"\n        response = self.chatbot.get_response(u'田中さんにあげて下さい')\n        self.assertGreater(len(response.text), 0)\n\n    def test_get_response_corrupted_text(self):\n        \"\"\"\n        Test the case that a string contains \"corrupted\" text.\n        \"\"\"\n        response = self.chatbot.get_response(u'Ṱ̺̺̕h̼͓̲̦̳̘̲e͇̣̰̦̬͎ ̢̼̻̱̘h͚͎͙̜̣̲ͅi̦̲̣̰̤v̻͍e̺̭̳̪̰-m̢iͅn̖̺̞̲̯̰d̵̼̟͙̩̼̘̳.̨̹͈̣')\n        self.assertGreater(len(response.text), 0)\n\n    def test_response_with_tags_added(self):\n        \"\"\"\n        If an input statement has tags added to it,\n        that data should saved with the input statement.\n        \"\"\"\n        self.chatbot.get_response(Statement(\n            text='Hello',\n            in_response_to='Hi',\n            tags=['test']\n        ))\n\n        results = list(self.chatbot.storage.filter(text='Hello'))\n\n        self.assertIsLength(results, 2)\n        self.assertIn('test', results[0].get_tags())\n\n    def test_response_preserves_tags(self):\n        \"\"\"\n        The response returned from the chatbot should preserve tags\n        from the statement that was used to generated the response.\n        \"\"\"\n        self._create_with_search_text(text='Hello', tags=['test'])\n        response = self.chatbot.get_response('Hello')\n\n        self.assertEqual(response.get_tags(), ['test'])\n\n    def test_get_response_with_text_and_kwargs(self):\n        self.chatbot.get_response('Hello', conversation='greetings')\n\n        results = list(self.chatbot.storage.filter(text='Hello'))\n\n        self.assertIsLength(results, 2)\n        self.assertEqual(results[0].conversation, 'greetings')\n\n    def test_get_response_missing_text(self):\n        with self.assertRaises(self.chatbot.ChatBotException):\n            self.chatbot.get_response()\n\n    def test_get_response_missing_text_with_conversation(self):\n        with self.assertRaises(self.chatbot.ChatBotException):\n            self.chatbot.get_response(conversation='test')\n\n    def test_generate_response(self):\n        statement = Statement(text='Many insects adopt a tripedal gait for rapid yet stable walking.')\n        response = self.chatbot.generate_response(statement)\n\n        self.assertEqual(response.text, statement.text)\n        self.assertEqual(response.confidence, 0)\n\n    def test_learn_response(self):\n        previous_response = Statement(text='Define Hemoglobin.')\n        statement = Statement(text='Hemoglobin is an oxygen-transport metalloprotein.')\n        self.chatbot.learn_response(statement, previous_response)\n        results = list(self.chatbot.storage.filter(text=statement.text))\n\n        self.assertIsLength(results, 1)\n\n    def test_get_response_does_not_add_new_statement(self):\n        \"\"\"\n        Test that a new statement is not learned if `read_only` is set to True.\n        \"\"\"\n        self.chatbot.read_only = True\n        self.chatbot.get_response('Hi!')\n        results = list(self.chatbot.storage.filter(text='Hi!'))\n\n        self.assertIsLength(results, 0)\n\n    def test_get_latest_response_from_zero_responses(self):\n        response = self.chatbot.get_latest_response('invalid')\n\n        self.assertIsNone(response)\n\n    def test_get_latest_response_from_one_responses(self):\n        self._create_with_search_text(text='A', conversation='test')\n        self._create_with_search_text(text='B', conversation='test', in_response_to='A')\n\n        response = self.chatbot.get_latest_response('test')\n\n        self.assertEqual(response.text, 'B')\n\n    def test_get_latest_response_from_two_responses(self):\n        self._create_with_search_text(text='A', conversation='test')\n        self._create_with_search_text(text='B', conversation='test', in_response_to='A')\n        self._create_with_search_text(text='C', conversation='test', in_response_to='B')\n\n        response = self.chatbot.get_latest_response('test')\n\n        self.assertEqual(response.text, 'C')\n\n    def test_get_latest_response_from_three_responses(self):\n        self._create_with_search_text(text='A', conversation='test')\n        self._create_with_search_text(text='B', conversation='test', in_response_to='A')\n        self._create_with_search_text(text='C', conversation='test', in_response_to='B')\n        self._create_with_search_text(text='D', conversation='test', in_response_to='C')\n\n        response = self.chatbot.get_latest_response('test')\n\n        self.assertEqual(response.text, 'D')\n\n    def test_search_text_results_after_training(self):\n        \"\"\"\n        ChatterBot should return close matches to an input\n        string when filtering using the search_text parameter.\n        \"\"\"\n        self._create_many_with_search_text([\n            Statement('Example A for search.'),\n            Statement('Another example.'),\n            Statement('Example B for search.'),\n            Statement(text='Another statement.'),\n        ])\n\n        results = list(self.chatbot.storage.filter(\n            search_text=self.chatbot.tagger.get_text_index_string(\n                'Example A for search.'\n            )\n        ))\n\n        self.assertEqual(len(results), 1)\n        self.assertEqual('Example A for search.', results[0].text)\n\n\nclass TestAdapterA(LogicAdapter):\n\n    def process(self, statement, additional_response_selection_parameters=None):\n        response = Statement(text='Good morning.')\n        response.confidence = 0.2\n        return response\n\n\nclass TestAdapterB(LogicAdapter):\n\n    def process(self, statement, additional_response_selection_parameters=None):\n        response = Statement(text='Good morning.')\n        response.confidence = 0.5\n        return response\n\n\nclass TestAdapterC(LogicAdapter):\n\n    def process(self, statement, additional_response_selection_parameters=None):\n        response = Statement(text='Good night.')\n        response.confidence = 0.7\n        return response\n\n\nclass ChatBotLogicAdapterTestCase(ChatBotTestCase):\n\n    def test_sub_adapter_agreement(self):\n        \"\"\"\n        In the case that multiple adapters agree on a given\n        statement, this statement should be returned with the\n        highest confidence available from these matching options.\n        \"\"\"\n        self.chatbot.logic_adapters = [\n            TestAdapterA(self.chatbot),\n            TestAdapterB(self.chatbot),\n            TestAdapterC(self.chatbot)\n        ]\n\n        statement = self.chatbot.generate_response(Statement(text='Howdy!'))\n\n        self.assertEqual(statement.confidence, 0.5)\n        self.assertEqual(statement.text, 'Good morning.')\n\n    def test_chatbot_set_for_all_logic_adapters(self):\n        for sub_adapter in self.chatbot.logic_adapters:\n            self.assertEqual(sub_adapter.chatbot, self.chatbot)\n        self.assertGreater(\n            len(self.chatbot.logic_adapters), 0,\n            msg='At least one logic adapter is expected for this test.'\n        )\n\n    def test_response_persona_is_bot(self):\n        \"\"\"\n        The response returned from the chatbot should be set to the name of the chatbot.\n        \"\"\"\n        response = self.chatbot.get_response('Hey everyone!')\n\n        self.assertEqual(response.persona, 'bot:Test Bot')\n"
  },
  {
    "path": "tests/test_cli.py",
    "content": "from unittest import TestCase\nfrom chatterbot import __main__ as main\n\n\nclass CommandLineInterfaceTests(TestCase):\n    \"\"\"\n    Tests for the command line tools that are included with ChatterBot.\n    \"\"\"\n\n    def test_get_chatterbot_version(self):\n        version = main.get_chatterbot_version()\n        version_parts = version.split('.')\n        self.assertEqual(len(version_parts), 3)\n        self.assertTrue(version_parts[0].isdigit())\n        self.assertTrue(version_parts[1].isdigit())\n"
  },
  {
    "path": "tests/test_comparisons.py",
    "content": "\"\"\"\nTest ChatterBot's statement comparison algorithms.\n\"\"\"\n\nfrom unittest import TestCase, skip\nfrom chatterbot.conversation import Statement\nfrom chatterbot import comparisons\nfrom chatterbot import languages\n\n\nclass LevenshteinDistanceTestCase(TestCase):\n\n    def setUp(self):\n        super().setUp()\n\n        self.compare = comparisons.LevenshteinDistance(\n            language=languages.ENG\n        )\n\n    def test_levenshtein_distance_statement_false(self):\n        \"\"\"\n        Falsy values should match by zero.\n        \"\"\"\n        statement = Statement(text='')\n        other_statement = Statement(text='Hello')\n\n        value = self.compare(statement, other_statement)\n\n        self.assertEqual(value, 0)\n\n    def test_levenshtein_distance_other_statement_false(self):\n        \"\"\"\n        Falsy values should match by zero.\n        \"\"\"\n        statement = Statement(text='Hello')\n        other_statement = Statement(text='')\n\n        value = self.compare(statement, other_statement)\n\n        self.assertEqual(value, 0)\n\n    def test_levenshtein_distance_statement_integer(self):\n        \"\"\"\n        Test that an exception is not raised if a statement is initialized\n        with an integer value as its text attribute.\n        \"\"\"\n        statement = Statement(text=2)\n        other_statement = Statement(text='Hello')\n\n        value = self.compare(statement, other_statement)\n\n        self.assertEqual(value, 0)\n\n    def test_exact_match_different_capitalization(self):\n        \"\"\"\n        Test that text capitalization is ignored.\n        \"\"\"\n        statement = Statement(text='Hi HoW ArE yOu?')\n        other_statement = Statement(text='hI hOw are YoU?')\n\n        value = self.compare(statement, other_statement)\n\n        self.assertEqual(value, 1)\n\n\nclass SpacySimilarityTests(TestCase):\n\n    def setUp(self):\n        super().setUp()\n\n        self.compare = comparisons.SpacySimilarity(\n            language=languages.ENG\n        )\n\n    @skip('TODO: Update assertion & re-enable')\n    def test_exact_match_different_stopwords(self):\n        \"\"\"\n        Test sentences with different stopwords.\n        \"\"\"\n        statement = Statement(text='What is matter?')\n        other_statement = Statement(text='What is the matter?')\n\n        value = self.compare(statement, other_statement)\n\n        self.assertAlmostEqual(value, 0.7, places=1)\n\n    def test_exact_match_different_capitalization(self):\n        \"\"\"\n        Test that text capitalization is ignored.\n        \"\"\"\n        statement = Statement(text='Hi HoW ArE yOu?')\n        other_statement = Statement(text='hI hOw are YoU?')\n\n        value = self.compare(statement, other_statement)\n\n        self.assertAlmostEqual(value, 0.8, places=1)\n\n\nclass JaccardSimilarityTestCase(TestCase):\n\n    def setUp(self):\n        super().setUp()\n\n        self.compare = comparisons.JaccardSimilarity(\n            language=languages.ENG\n        )\n\n    def test_exact_match_different_capitalization(self):\n        \"\"\"\n        Test that text capitalization is ignored.\n        \"\"\"\n        statement = Statement(text='Hi HoW ArE yOu?')\n        other_statement = Statement(text='hI hOw are YoU?')\n\n        value = self.compare(statement, other_statement)\n\n        self.assertEqual(value, 1)\n"
  },
  {
    "path": "tests/test_conversations.py",
    "content": "from unittest import TestCase\nfrom tests.base_case import ChatBotTestCase\nfrom chatterbot import ChatBot\nfrom chatterbot.conversation import Statement\n\n\nclass StatementTests(TestCase):\n\n    def setUp(self):\n        self.statement = Statement(text='A test statement.')\n\n    def test_serializer(self):\n        data = self.statement.serialize()\n        self.assertEqual(self.statement.text, data['text'])\n\n\nclass DefaultConversationTestCase(ChatBotTestCase):\n    \"\"\"\n    Test that the ChatBot assigns a default conversation ID when none is\n    provided, so that LLM adapters can retrieve conversation history.\n    \"\"\"\n\n    def test_default_conversation_is_set(self):\n        \"\"\"\n        The ChatBot should have a default_conversation attribute\n        that is a 32-character hex string (UUID without hyphens).\n        \"\"\"\n        self.assertIsNotNone(self.chatbot.default_conversation)\n        self.assertEqual(len(self.chatbot.default_conversation), 32)\n        # Verify it's a valid hex string\n        int(self.chatbot.default_conversation, 16)\n\n    def test_default_conversation_is_unique_per_instance(self):\n        \"\"\"\n        Each ChatBot instance should generate a unique conversation ID.\n        \"\"\"\n        kwargs = self.get_kwargs()\n        kwargs['tagger'] = self._shared_tagger\n        other_bot = ChatBot('Other Bot', **kwargs)\n\n        self.assertNotEqual(\n            self.chatbot.default_conversation,\n            other_bot.default_conversation\n        )\n\n        other_bot.storage.drop()\n        other_bot.storage.close()\n\n    def test_response_gets_default_conversation(self):\n        \"\"\"\n        When no conversation kwarg is passed, the response should\n        have the chatbot's default_conversation ID assigned.\n        \"\"\"\n        response = self.chatbot.get_response('Hello')\n        self.assertEqual(response.conversation, self.chatbot.default_conversation)\n\n    def test_explicit_conversation_overrides_default(self):\n        \"\"\"\n        When an explicit conversation ID is provided, it should be\n        used instead of the default_conversation.\n        \"\"\"\n        response = self.chatbot.get_response('Hello', conversation='my-convo')\n        self.assertEqual(response.conversation, 'my-convo')\n\n    def test_statement_object_conversation_overrides_default(self):\n        \"\"\"\n        When a Statement object with a conversation ID is provided,\n        it should be used instead of the default_conversation.\n        \"\"\"\n        statement = Statement(text='Hello', conversation='custom-id')\n        response = self.chatbot.get_response(statement)\n        self.assertEqual(response.conversation, 'custom-id')\n\n    def test_empty_conversation_gets_default(self):\n        \"\"\"\n        When a Statement object with an empty conversation string is\n        provided, the default should be applied as a fallback.\n        \"\"\"\n        statement = Statement(text='Hello')\n        # Statement defaults to '' for conversation\n        self.assertEqual(statement.conversation, '')\n\n        response = self.chatbot.get_response(statement)\n        self.assertEqual(response.conversation, self.chatbot.default_conversation)\n\n    def test_statements_saved_with_default_conversation(self):\n        \"\"\"\n        Both the input and response statements should be saved\n        to storage with the default conversation ID.\n        \"\"\"\n        self.chatbot.get_response('Hello')\n        results = list(self.chatbot.storage.filter(\n            conversation=self.chatbot.default_conversation\n        ))\n        # Should have at least the input and the response\n        self.assertGreaterEqual(len(results), 2)\n\n    def test_conversation_history_accumulates(self):\n        \"\"\"\n        Multiple calls with the same default conversation should\n        accumulate in storage, allowing history retrieval.\n        \"\"\"\n        self.chatbot.get_response('First message')\n        self.chatbot.get_response('Second message')\n\n        results = list(self.chatbot.storage.filter(\n            conversation=self.chatbot.default_conversation,\n            order_by=['id']\n        ))\n\n        # Each get_response saves the input + learned response = 2 per call\n        # so 2 calls should produce at least 4 statements\n        self.assertGreaterEqual(len(results), 4)\n\n        texts = [r.text for r in results]\n        self.assertIn('First message', texts)\n        self.assertIn('Second message', texts)\n"
  },
  {
    "path": "tests/test_corpus.py",
    "content": "import os\nimport io\nfrom unittest import TestCase\nfrom chatterbot import corpus\n\n\nclass CorpusLoadingTestCase(TestCase):\n\n    def test_load_corpus_chinese(self):\n        data_files = corpus.list_corpus_files('chatterbot.corpus.chinese')\n        corpus_data = corpus.load_corpus(*data_files)\n\n        self.assertTrue(len(list(corpus_data)))\n\n    def test_load_corpus_english(self):\n        data_files = corpus.list_corpus_files('chatterbot.corpus.english')\n        corpus_data = corpus.load_corpus(*data_files)\n\n        self.assertTrue(len(list(corpus_data)))\n\n    def test_load_corpus_english_greetings(self):\n        data_files = corpus.list_corpus_files('chatterbot.corpus.english.greetings')\n        corpus_data = list(corpus.load_corpus(*data_files))\n\n        self.assertEqual(len(corpus_data), 1)\n\n        conversations, categories, file_path = corpus_data[0]\n\n        self.assertIn(['Hi', 'Hello'], conversations)\n        self.assertEqual(['greetings'], categories)\n        self.assertIn('chatterbot_corpus/data/english/greetings.yml', file_path)\n\n    def test_load_corpus_english_categories(self):\n        data_files = corpus.list_corpus_files('chatterbot.corpus.english.greetings')\n        corpus_data = list(corpus.load_corpus(*data_files))\n\n        self.assertEqual(len(corpus_data), 1)\n\n        # Test that each conversation gets labeled with the correct category\n        for _conversation, categories, _file_path in corpus_data:\n            self.assertIn('greetings', categories)\n\n    def test_load_corpus_french(self):\n        data_files = corpus.list_corpus_files('chatterbot.corpus.french')\n        corpus_data = corpus.load_corpus(*data_files)\n\n        self.assertTrue(len(list(corpus_data)))\n\n    def test_load_corpus_german(self):\n        data_files = corpus.list_corpus_files('chatterbot.corpus.german')\n        corpus_data = corpus.load_corpus(*data_files)\n\n        self.assertTrue(len(list(corpus_data)))\n\n    def test_load_corpus_hindi(self):\n        data_files = corpus.list_corpus_files('chatterbot.corpus.hindi')\n        corpus_data = corpus.load_corpus(*data_files)\n\n        self.assertTrue(len(list(corpus_data)))\n\n    def test_load_corpus_indonesian(self):\n        data_files = corpus.list_corpus_files('chatterbot.corpus.indonesian')\n        corpus_data = corpus.load_corpus(*data_files)\n\n        self.assertTrue(len(list(corpus_data)))\n\n    def test_load_corpus_italian(self):\n        data_files = corpus.list_corpus_files('chatterbot.corpus.italian')\n        corpus_data = corpus.load_corpus(*data_files)\n\n        self.assertTrue(len(list(corpus_data)))\n\n    def test_load_corpus_marathi(self):\n        data_files = corpus.list_corpus_files('chatterbot.corpus.marathi')\n        corpus_data = corpus.load_corpus(*data_files)\n\n        self.assertTrue(len(list(corpus_data)))\n\n    def test_load_corpus_portuguese(self):\n        data_files = corpus.list_corpus_files('chatterbot.corpus.portuguese')\n        corpus_data = corpus.load_corpus(*data_files)\n\n        self.assertTrue(len(list(corpus_data)))\n\n    def test_load_corpus_russian(self):\n        data_files = corpus.list_corpus_files('chatterbot.corpus.russian')\n        corpus_data = corpus.load_corpus(*data_files)\n\n        self.assertTrue(len(list(corpus_data)))\n\n    def test_load_corpus_spanish(self):\n        data_files = corpus.list_corpus_files('chatterbot.corpus.spanish')\n        corpus_data = corpus.load_corpus(*data_files)\n\n        self.assertTrue(len(list(corpus_data)))\n\n    def test_load_corpus_telugu(self):\n        data_files = corpus.list_corpus_files('chatterbot.corpus.telugu')\n        corpus_data = corpus.load_corpus(*data_files)\n\n        self.assertTrue(len(list(corpus_data)))\n\n\nclass CorpusUtilsTestCase(TestCase):\n\n    def test_get_file_path(self):\n        \"\"\"\n        Test that a dotted path is properly converted to a file address.\n        \"\"\"\n        path = corpus.get_file_path('chatterbot.corpus.english')\n        self.assertIn(\n            os.path.join('chatterbot_corpus', 'data', 'english'),\n            path\n        )\n\n    def test_read_english_corpus(self):\n        corpus_path = os.path.join(\n            corpus.DATA_DIRECTORY,\n            'english', 'conversations.yml'\n        )\n        data = corpus.read_corpus(corpus_path)\n        self.assertIn('conversations', data)\n\n    def test_list_english_corpus_files(self):\n        data_files = corpus.list_corpus_files('chatterbot.corpus.english')\n\n        for data_file in data_files:\n            self.assertIn('.yml', data_file)\n\n    def test_load_corpus(self):\n        \"\"\"\n        Test loading the entire corpus of languages.\n        \"\"\"\n        corpus_files = corpus.list_corpus_files('chatterbot.corpus')\n        corpus_data = corpus.load_corpus(*corpus_files)\n\n        self.assertTrue(len(list(corpus_data)))\n\n\nclass CorpusFilePathTestCase(TestCase):\n\n    def test_load_corpus_file(self):\n        \"\"\"\n        Test that a file path can be specified for a corpus.\n        \"\"\"\n\n        # Create a file for testing\n        file_path = './test_corpus.yml'\n        with io.open(file_path, 'w') as test_corpus:\n            yml_data = u'\\n'.join(\n                ['conversations:', '- - Hello', '  - Hi', '- - Hi', '  - Hello']\n            )\n            test_corpus.write(yml_data)\n\n        data_files = corpus.list_corpus_files(file_path)\n        corpus_data = list(corpus.load_corpus(*data_files))\n\n        # Remove the test file\n        if os.path.exists(file_path):\n            os.remove(file_path)\n\n        self.assertEqual(len(corpus_data), 1)\n\n        # Load the content from the corpus\n        conversations, _categories, _file_path = corpus_data[0]\n\n        self.assertEqual(len(conversations[0]), 2)\n\n    def test_load_corpus_file_non_existent(self):\n        \"\"\"\n        Test that a file path can be specified for a corpus.\n        \"\"\"\n        file_path = './test_corpus.yml'\n\n        self.assertFalse(os.path.exists(file_path))\n        with self.assertRaises(IOError):\n            list(corpus.load_corpus(file_path))\n\n    def test_load_corpus_english_greetings(self):\n        file_path = os.path.join(corpus.DATA_DIRECTORY, 'english', 'greetings.yml')\n        data_files = corpus.list_corpus_files(file_path)\n        corpus_data = corpus.load_corpus(*data_files)\n\n        self.assertEqual(len(list(corpus_data)), 1)\n\n    def test_load_corpus_english(self):\n        file_path = os.path.join(corpus.DATA_DIRECTORY, 'english')\n        data_files = corpus.list_corpus_files(file_path)\n        corpus_data = corpus.load_corpus(*data_files)\n\n        self.assertGreater(len(list(corpus_data)), 1)\n\n    def test_load_corpus_english_trailing_slash(self):\n        file_path = os.path.join(corpus.DATA_DIRECTORY, 'english') + '/'\n        data_files = corpus.list_corpus_files(file_path)\n        corpus_data = list(corpus.load_corpus(*data_files))\n\n        self.assertGreater(len(list(corpus_data)), 1)\n"
  },
  {
    "path": "tests/test_examples.py",
    "content": "from unittest import TestCase\n\n\nclass ExamplesSmokeTestCase(TestCase):\n    \"\"\"\n    These are just basic tests that run each example\n    to make sure no errors are triggered.\n    \"\"\"\n\n    def test_basic_example(self):\n        from examples import basic_example # NOQA\n\n    def test_convert_units(self):\n        from examples import convert_units # NOQA\n\n    def test_default_response_example(self):\n        from examples import default_response_example # NOQA\n\n    def test_export_example(self):\n        self.skipTest(\n            'This is being skipped to avoid creating files during tests.'\n        )\n\n    def test_learning_feedback_example(self):\n        self.skipTest(\n            'This is being skipped because it contains '\n            'a while loop in the code body and will not '\n            'terminate on its own.'\n        )\n\n    def test_math_and_time(self):\n        from examples import math_and_time # NOQA\n\n    def test_memory_sql_example(self):\n        from examples import memory_sql_example # NOQA\n\n    def test_specific_response_example(self):\n        from examples import specific_response_example # NOQA\n\n    def test_tagged_dataset_example(self):\n        from examples import tagged_dataset_example # NOQA\n\n    def test_terminal_example(self):\n        self.skipTest(\n            'This is being skipped because it contains '\n            'a while loop in the code body and will not '\n            'terminate on its own.'\n        )\n\n    def test_terminal_mongo_example(self):\n        self.skipTest(\n            'This is being skipped so that we do not have '\n            'to check if Mongo DB is running before running '\n            'this test.'\n        )\n\n    def test_tkinter_gui(self):\n        self.skipTest(\n            'This is being skipped so that we do not open up '\n            'a GUI during testing.'\n        )\n\n    def test_training_example_chatterbot_corpus(self):\n        from examples import training_example_chatterbot_corpus # NOQA\n\n    def test_training_example_list_data(self):\n        from examples import training_example_list_data # NOQA\n\n    def test_training_example_ubuntu_corpus(self):\n        self.skipTest(\n            'This test is being skipped because it takes '\n            'hours to download and train from this corpus.'\n        )\n"
  },
  {
    "path": "tests/test_filters.py",
    "content": "from tests.base_case import ChatBotMongoTestCase\nfrom chatterbot import filters\n\n\nclass RepetitiveResponseFilterTestCase(ChatBotMongoTestCase):\n    \"\"\"\n    Test case for the repetitive response filter.\n    \"\"\"\n\n    def test_filter_selection(self):\n        \"\"\"\n        Test that repetitive responses are filtered out of the results.\n        \"\"\"\n        from chatterbot.conversation import Statement\n        from chatterbot.trainers import ListTrainer\n\n        self.chatbot.filters = (filters.get_recent_repeated_responses, )\n\n        self.trainer = ListTrainer(\n            self.chatbot,\n            show_training_progress=False\n        )\n\n        self.trainer.train([\n            'Hi',\n            'Hello',\n            'Hi',\n            'Hello',\n            'Hi',\n            'Hello',\n            'How are you?',\n            'I am good',\n            'Glad to hear',\n            'How are you?'\n        ])\n\n        statement = Statement(text='Hello', conversation='training')\n        first_response = self.chatbot.get_response(statement)\n        second_response = self.chatbot.get_response(statement)\n\n        self.assertEqual('How are you?', first_response.text)\n        self.assertEqual('Hi', second_response.text)\n"
  },
  {
    "path": "tests/test_initialization.py",
    "content": "from tests.base_case import ChatBotTestCase\n\n\nclass StringInitializationTestCase(ChatBotTestCase):\n\n    def get_kwargs(self):\n        return {\n            'storage_adapter': 'chatterbot.storage.SQLStorageAdapter',\n            'database_uri': None\n        }\n\n    def test_storage_initialized(self):\n        from chatterbot.storage import SQLStorageAdapter\n        self.assertTrue(isinstance(self.chatbot.storage, SQLStorageAdapter))\n\n    def test_logic_initialized(self):\n        from chatterbot.logic import BestMatch\n        self.assertEqual(len(self.chatbot.logic_adapters), 1)\n        self.assertTrue(isinstance(self.chatbot.logic_adapters[0], BestMatch))\n\n\nclass DictionaryInitializationTestCase(ChatBotTestCase):\n\n    def get_kwargs(self):\n        return {\n            'storage_adapter': {\n                'import_path': 'chatterbot.storage.SQLStorageAdapter',\n                'database_uri': None\n            },\n            'logic_adapters': [\n                {\n                    'import_path': 'chatterbot.logic.BestMatch',\n                },\n                {\n                    'import_path': 'chatterbot.logic.MathematicalEvaluation',\n                }\n            ]\n        }\n\n    def test_storage_initialized(self):\n        from chatterbot.storage import SQLStorageAdapter\n        self.assertTrue(isinstance(self.chatbot.storage, SQLStorageAdapter))\n\n    def test_logic_initialized(self):\n        from chatterbot.logic import BestMatch\n        from chatterbot.logic import MathematicalEvaluation\n        self.assertEqual(len(self.chatbot.logic_adapters), 2)\n        self.assertTrue(isinstance(self.chatbot.logic_adapters[0], BestMatch))\n        self.assertTrue(isinstance(self.chatbot.logic_adapters[1], MathematicalEvaluation))\n"
  },
  {
    "path": "tests/test_languages.py",
    "content": "import inspect\nfrom chatterbot import languages\nfrom unittest import TestCase\n\n\nclass LanguageClassTests(TestCase):\n\n    def test_classes_have_correct_attributes(self):\n        language_classes = languages.get_language_classes()\n\n        for name, obj in language_classes:\n            self.assertTrue(inspect.isclass(obj))\n            self.assertTrue(hasattr(obj, 'ISO_639'))\n            self.assertTrue(hasattr(obj, 'ISO_639_1'))\n            self.assertTrue(hasattr(obj, 'ENGLISH_NAME'))\n            self.assertEqual(name, obj.ISO_639.upper())\n\n        self.assertEqual(len(language_classes), 402)\n"
  },
  {
    "path": "tests/test_parsing.py",
    "content": "from unittest import TestCase\nfrom datetime import timedelta, datetime\nfrom chatterbot import parsing\n\n\nclass DateTimeParsingFunctionIntegrationTestCases(TestCase):\n    \"\"\"\n    Test the datetime parsing module.\n\n    Output of the parser is an array of tuples\n    [match, value, (start, end)]\n    \"\"\"\n\n    def setUp(self):\n        super().setUp()\n        self.base_date = datetime.now()\n\n    def test_captured_pattern_is_on_date(self):\n        input_text = 'The event is on Monday 12 January 2012'\n        parser = parsing.datetime_parsing(input_text)\n        self.assertIn('Monday 12 January 2012', parser[0])\n        self.assertEqual(parser[0][1], datetime(2012, 1, 12))\n        self.assertEqual(len(parser), 1)\n\n    def test_captured_pattern_this_weekday(self):\n        input_text = 'This monday'\n        parser = parsing.datetime_parsing(input_text)\n        self.assertIn(input_text, parser[0])\n        self.assertEqual(\n            parser[0][1].strftime('%d-%m-%y'),\n            parsing.this_week_day(self.base_date, 0).strftime('%d-%m-%y')\n        )\n        self.assertEqual(len(parser), 1)\n\n    def test_captured_pattern_last_weekday(self):\n        input_text = 'Last monday'\n        parser = parsing.datetime_parsing(input_text)\n        self.assertIn(input_text, parser[0])\n        self.assertEqual(\n            parser[0][1].strftime('%d-%m-%y'),\n            parsing.previous_week_day(self.base_date, 0).strftime('%d-%m-%y')\n        )\n        self.assertEqual(len(parser), 1)\n\n    def test_captured_pattern_next_weekday(self):\n        input_text = 'Next monday'\n        parser = parsing.datetime_parsing(input_text)\n        self.assertIn(input_text, parser[0])\n        self.assertEqual(\n            parser[0][1].strftime('%d-%m-%y'),\n            parsing.next_week_day(self.base_date, 0).strftime('%d-%m-%y')\n        )\n        self.assertEqual(len(parser), 1)\n\n    def test_captured_pattern_minutes_from_now(self):\n        input_text = '25 minutes from now'\n        parser = parsing.datetime_parsing(input_text)\n        self.assertIn(input_text, parser[0])\n        self.assertEqual(\n            parser[0][1].strftime('%d-%m-%y'),\n            parsing.date_from_duration(\n                self.base_date, 25, 'minutes', 'from now'\n            ).strftime('%d-%m-%y')\n        )\n        self.assertEqual(len(parser), 1)\n\n    def test_captured_pattern_days_later(self):\n        input_text = '10 days later'\n        parser = parsing.datetime_parsing(input_text)\n        self.assertIn(input_text, parser[0])\n        self.assertEqual(\n            parser[0][1].strftime('%d-%m-%y'),\n            parsing.date_from_duration(self.base_date, 10, 'days', 'later').strftime('%d-%m-%y')\n        )\n        self.assertEqual(len(parser), 1)\n\n    def test_captured_pattern_year(self):\n        input_text = '2010'\n        parser = parsing.datetime_parsing(input_text)\n        self.assertIn(input_text, parser[0])\n        self.assertEqual(parser[0][1].strftime('%Y'), input_text)\n        self.assertEqual(len(parser), 1)\n\n    def test_captured_pattern_today(self):\n        input_text = 'today'\n        parser = parsing.datetime_parsing(input_text)\n        self.assertIn(input_text, parser[0])\n        self.assertEqual(parser[0][1].strftime('%d'), datetime.today().strftime('%d'))\n        self.assertEqual(len(parser), 1)\n\n    def test_captured_pattern_tomorrow(self):\n        input_text = 'tomorrow'\n        parser = parsing.datetime_parsing(input_text)\n        self.assertIn(input_text, parser[0])\n        self.assertEqual(\n            parser[0][1].strftime('%d'),\n            (datetime.today() + timedelta(days=1)).strftime('%d')\n        )\n        self.assertEqual(len(parser), 1)\n\n    def test_captured_pattern_yesterday(self):\n        input_text = 'yesterday'\n        parser = parsing.datetime_parsing(input_text)\n        self.assertIn(input_text, parser[0])\n        self.assertEqual(\n            parser[0][1].strftime('%d'),\n            (datetime.today() - timedelta(days=1)).strftime('%d')\n        )\n        self.assertEqual(len(parser), 1)\n\n    def test_captured_pattern_before_yesterday(self):\n        input_text = 'day before yesterday'\n        parser = parsing.datetime_parsing(input_text)\n        self.assertIn(input_text, parser[0])\n        self.assertEqual(\n            parser[0][1].strftime('%d'),\n            (datetime.today() - timedelta(days=2)).strftime('%d')\n        )\n        self.assertEqual(len(parser), 1)\n\n    def test_captured_pattern_before_today(self):\n        input_text = 'day before today'\n        parser = parsing.datetime_parsing(input_text)\n        self.assertIn(input_text, parser[0])\n        self.assertEqual(\n            parser[0][1].strftime('%d'),\n            (datetime.today() - timedelta(days=1)).strftime('%d')\n        )\n        self.assertEqual(len(parser), 1)\n\n    def test_captured_pattern_before_tomorrow(self):\n        input_text = 'day before tomorrow'\n        parser = parsing.datetime_parsing(input_text)\n        self.assertIn(input_text, parser[0])\n        self.assertEqual(\n            parser[0][1].strftime('%d'),\n            (datetime.today() - timedelta(days=0)).strftime('%d')\n        )\n        self.assertEqual(len(parser), 1)\n\n        input_text = '2 days before'\n        parser = parsing.datetime_parsing(input_text)\n        self.assertIn(input_text, parser[0])\n        self.assertEqual(\n            parser[0][1].strftime('%d'),\n            (datetime.today() - timedelta(days=2)).strftime('%d')\n        )\n        self.assertEqual(len(parser), 1)\n\n    def test_captured_pattern_two_days(self):\n        input_text = 'Monday and Friday'\n        parser = parsing.datetime_parsing(input_text)\n        self.assertIn('Monday', parser[0])\n        self.assertIn('Friday', parser[1])\n        self.assertEqual(\n            parser[0][1].strftime('%d'),\n            parsing.this_week_day(self.base_date, 0).strftime('%d')\n        )\n        self.assertEqual(\n            parser[1][1].strftime('%d'),\n            parsing.this_week_day(self.base_date, 4).strftime('%d')\n        )\n        self.assertEqual(len(parser), 2)\n\n    def test_captured_pattern_first_quarter_of_year(self):\n        input_text = 'First quarter of 2016'\n        parser = parsing.datetime_parsing(input_text)\n        self.assertIn(input_text, parser[0])\n        self.assertEqual(parser[0][1][0].strftime('%d-%m-%Y'), '01-01-2016')\n        self.assertEqual(parser[0][1][1].strftime('%d-%m-%Y'), '31-03-2016')\n        self.assertEqual(len(parser), 1)\n\n    def test_captured_pattern_last_quarter_of_year(self):\n        input_text = 'Last quarter of 2015'\n        parser = parsing.datetime_parsing(input_text)\n        self.assertIn(input_text, parser[0])\n        self.assertEqual(parser[0][1][0].strftime('%d-%m-%Y'), '01-09-2015')\n        self.assertEqual(parser[0][1][1].strftime('%d-%m-%Y'), '31-12-2015')\n        self.assertEqual(len(parser), 1)\n\n    def test_captured_pattern_is_next_three_weeks(self):\n        input_text = 'Next 3 weeks'\n        parser = parsing.datetime_parsing(input_text)\n        self.assertIn(input_text, parser[0])\n        self.assertEqual(\n            parser[0][1].strftime('%d-%m-%Y'),\n            (datetime.today() + timedelta(weeks=3)).strftime('%d-%m-%Y')\n        )\n        self.assertEqual(len(parser), 1)\n\n    def test_captured_pattern_is_next_x_weeks_case_insensitive(self):\n        input_text = 'next 2 Weeks'\n        parser = parsing.datetime_parsing(input_text)\n        self.assertIn(input_text, parser[0])\n        self.assertEqual(\n            parser[0][1].strftime('%d-%m-%Y'),\n            (datetime.today() + timedelta(weeks=2)).strftime('%d-%m-%Y')\n        )\n        self.assertEqual(len(parser), 1)\n\n    def test_captured_pattern_is_next_eight_days(self):\n        input_text = 'Next 8 days'\n        parser = parsing.datetime_parsing(input_text)\n        self.assertIn(input_text, parser[0])\n        self.assertEqual(\n            parser[0][1].strftime('%d-%m-%Y'),\n            (datetime.today() + timedelta(days=8)).strftime('%d-%m-%Y')\n        )\n        self.assertEqual(len(parser), 1)\n\n    def test_captured_pattern_is_next_x_days_case_insensitive(self):\n        input_text = 'next 14 Days'\n        parser = parsing.datetime_parsing(input_text)\n        self.assertIn(input_text, parser[0])\n        self.assertEqual(\n            parser[0][1].strftime('%d-%m-%Y'),\n            (datetime.today() + timedelta(days=14)).strftime('%d-%m-%Y')\n        )\n        self.assertEqual(len(parser), 1)\n\n    def test_captured_pattern_is_next_ten_years(self):\n        input_text = 'Next 10 years'\n        parser = parsing.datetime_parsing(input_text)\n        self.assertIn('Next 10 year', parser[0])\n        self.assertEqual(\n            parser[0][1].strftime('%d-%m-%Y'),\n            (datetime.today() + timedelta(10 * 365)).strftime('%d-%m-%Y')\n        )\n        self.assertEqual(len(parser), 1)\n\n    def test_captured_pattern_is_next_x_years_case_insensitive(self):\n        input_text = 'next 43 Years'\n        parser = parsing.datetime_parsing(input_text)\n        self.assertIn('next 43 Year', parser[0])\n        self.assertEqual(\n            parser[0][1].strftime('%d-%m-%Y'),\n            (datetime.today() + timedelta(43 * 365)).strftime('%d-%m-%Y')\n        )\n        self.assertEqual(len(parser), 1)\n\n    def test_captured_pattern_is_next_eleven_months(self):\n        import calendar\n        input_text = 'Next 11 months'\n        parser = parsing.datetime_parsing(input_text)\n        relative_date = datetime.today()\n        month = relative_date.month - 1 + 11\n        year = relative_date.year + month // 12\n        month = month % 12 + 1\n        day = min(relative_date.day, calendar.monthrange(year, month)[1])\n        self.assertIn('Next 11 month', parser[0])\n        self.assertEqual(\n            parser[0][1].strftime('%d-%m-%Y'), datetime(year, month, day).strftime('%d-%m-%Y')\n        )\n        self.assertEqual(len(parser), 1)\n\n    def test_captured_pattern_is_next_x_months_case_insensitive(self):\n        import calendar\n        input_text = 'next 55 Months'\n        parser = parsing.datetime_parsing(input_text)\n        relative_date = datetime.today()\n        month = relative_date.month - 1 + 55\n        year = relative_date.year + month // 12\n        month = month % 12 + 1\n        day = min(relative_date.day, calendar.monthrange(year, month)[1])\n        self.assertIn('next 55 Month', parser[0])\n        self.assertEqual(\n            parser[0][1].strftime('%d-%m-%Y'), datetime(year, month, day).strftime('%d-%m-%Y')\n        )\n        self.assertEqual(len(parser), 1)\n\n    def test_captured_pattern_is_on_day(self):\n        input_text = 'My birthday is on January 2nd.'\n        parser = parsing.datetime_parsing(input_text)\n        self.assertIn('January 2nd', parser[0])\n        self.assertEqual(parser[0][1].month, 1)\n        self.assertEqual(parser[0][1].day, 2)\n        self.assertEqual(len(parser), 1)\n\n    def test_captured_pattern_is_on_day_of_year_variation1(self):\n        input_text = 'My birthday is on January 1st 2014.'\n        parser = parsing.datetime_parsing(input_text)\n        self.assertIn('January 1st 2014', parser[0])\n        self.assertEqual(parser[0][1].strftime('%d-%m-%Y'), '01-01-2014')\n        self.assertEqual(len(parser), 1)\n\n    def test_captured_pattern_is_on_day_of_year_variation2(self):\n        input_text = 'My birthday is on 2nd January 2014.'\n        parser = parsing.datetime_parsing(input_text)\n        self.assertIn('2nd January 2014', parser[0])\n        self.assertEqual(parser[0][1].strftime('%d-%m-%Y'), '02-01-2014')\n        self.assertEqual(len(parser), 1)\n\n    def test_captured_pattern_has_am(self):\n        input_text = 'You have to woke up at 5 am in the morning'\n        parser = parsing.datetime_parsing(input_text)\n        self.assertIn('5 am', parser[0])\n        self.assertEqual(parser[0][1].strftime('%d'), datetime.today().strftime('%d'))\n        self.assertEqual(parser[0][1].strftime('%H'), '05')\n        self.assertEqual(len(parser), 1)\n\n    def test_captured_pattern_has_am_case_insensitive_1(self):\n        input_text = '7 AM'\n        parser = parsing.datetime_parsing(input_text)\n        self.assertIn('7 AM', parser[0])\n        self.assertEqual(parser[0][1].strftime('%d'), datetime.today().strftime('%d'))\n        self.assertEqual(parser[0][1].strftime('%H'), '07')\n        self.assertEqual(len(parser), 1)\n\n    def test_captured_pattern_has_am_case_insensitive_2(self):\n        input_text = '1 Am'\n        parser = parsing.datetime_parsing(input_text)\n        self.assertIn('1 Am', parser[0])\n        self.assertEqual(parser[0][1].strftime('%d'), datetime.today().strftime('%d'))\n        self.assertEqual(parser[0][1].strftime('%H'), '01')\n        self.assertEqual(len(parser), 1)\n\n    def test_captured_pattern_has_am_case_insensitive_3(self):\n        input_text = '9aM'\n        parser = parsing.datetime_parsing(input_text)\n        self.assertIn('9aM', parser[0])\n        self.assertEqual(parser[0][1].strftime('%d'), datetime.today().strftime('%d'))\n        self.assertEqual(parser[0][1].strftime('%H'), '09')\n        self.assertEqual(len(parser), 1)\n\n    def test_captured_pattern_has_pm(self):\n        input_text = 'Your dental appointment at 4 pm in the evening.'\n        parser = parsing.datetime_parsing(input_text)\n        self.assertIn('4 pm', parser[0])\n        self.assertEqual(parser[0][1].strftime('%d'), datetime.today().strftime('%d'))\n        self.assertEqual(parser[0][1].strftime('%H'), '16')\n        self.assertEqual(len(parser), 1)\n\n    def test_captured_pattern_has_pm_case_insensitive_1(self):\n        input_text = '8 PM'\n        parser = parsing.datetime_parsing(input_text)\n        self.assertIn('8 PM', parser[0])\n        self.assertEqual(parser[0][1].strftime('%d'), datetime.today().strftime('%d'))\n        self.assertEqual(parser[0][1].strftime('%H'), '20')\n        self.assertEqual(len(parser), 1)\n\n    def test_captured_pattern_has_pm_case_insensitive_2(self):\n        input_text = '11 pM'\n        parser = parsing.datetime_parsing(input_text)\n        self.assertIn('11 pM', parser[0])\n        self.assertEqual(parser[0][1].strftime('%d'), datetime.today().strftime('%d'))\n        self.assertEqual(parser[0][1].strftime('%H'), '23')\n        self.assertEqual(len(parser), 1)\n\n    def test_captured_pattern_has_pm_case_insensitive_3(self):\n        input_text = '3Pm'\n        parser = parsing.datetime_parsing(input_text)\n        self.assertIn('3Pm', parser[0])\n        self.assertEqual(parser[0][1].strftime('%d'), datetime.today().strftime('%d'))\n        self.assertEqual(parser[0][1].strftime('%H'), '15')\n        self.assertEqual(len(parser), 1)\n\n\nclass DateTimeParsingTestCases(TestCase):\n    \"\"\"\n    Unit tests for datetime parsing functions.\n    \"\"\"\n\n    def test_next_week_day(self):\n        base_date = datetime(2016, 12, 7, 10, 10, 52, 85280)\n        weekday = 2  # Wednesday\n        result = parsing.next_week_day(base_date, weekday)\n\n        self.assertEqual(result, datetime(2016, 12, 14, 10, 10, 52, 85280))\n\n    def test_previous_week_day(self):\n        base_date = datetime(2016, 12, 14, 10, 10, 52, 85280)\n        weekday = 2  # Wednesday\n        result = parsing.previous_week_day(base_date, weekday)\n\n        self.assertEqual(result, datetime(2016, 12, 7, 10, 10, 52, 85280))\n\n    def test_this_week_day_before_day(self):\n        base_date = datetime(2016, 12, 5, 10, 10, 52, 85280)  # Monday\n        weekday = 2  # Wednesday\n        result = parsing.this_week_day(base_date, weekday)\n\n        self.assertEqual(result, datetime(2016, 12, 7, 10, 10, 52, 85280))\n\n    def test_this_week_day_after_day(self):\n        base_date = datetime(2016, 12, 9, 10, 10, 52, 85280)  # Friday\n        weekday = 2  # Wednesday\n        result = parsing.this_week_day(base_date, weekday)\n\n        self.assertEqual(result, datetime(2016, 12, 14, 10, 10, 52, 85280))\n\n    def test_today_at_time_uses_base_date(self):\n        \"\"\"\n        The string 'today at [time]' should respect the base_date parameter.\n        \"\"\"\n        base_date = datetime(2018, 7, 14, 8, 52, 21)  # July 14, 2018 at 8:52:21 AM\n        input_text = 'Your dental appointment is scheduled today at 9:00pm.'\n        parser = parsing.datetime_parsing(input_text, base_date)\n\n        self.assertEqual(len(parser), 1)\n        self.assertIn('today at 9:00pm', parser[0])\n\n        parsed_datetime = parser[0][1]\n        # Should be July 14, 2018 at 9:00 PM\n        self.assertEqual(parsed_datetime.year, 2018)\n        self.assertEqual(parsed_datetime.month, 7)\n        self.assertEqual(parsed_datetime.day, 14)\n        self.assertEqual(parsed_datetime.hour, 21)  # 9 PM\n        self.assertEqual(parsed_datetime.minute, 0)\n\n    def test_next_month_from_january_31st(self):\n        \"\"\"\n        Test 'next month' from Jan 31 handles February correctly\n        \"\"\"\n        base_date = datetime(2025, 1, 31, 12, 0, 0)\n        input_text = 'next month'\n        parser = parsing.datetime_parsing(input_text, base_date)\n\n        self.assertEqual(len(parser), 1)\n        # February only has 28 days in 2025, should clamp to last valid day\n        self.assertEqual(parser[0][1].year, 2025)\n        self.assertEqual(parser[0][1].month, 2)\n        self.assertEqual(parser[0][1].day, 28)\n\n    def test_next_3_months_crosses_year_boundary(self):\n        \"\"\"\n        Test 'next 3 months' crossing year boundary\n        \"\"\"\n        base_date = datetime(2025, 11, 15, 12, 0, 0)\n        input_text = 'next 3 months'\n        parser = parsing.datetime_parsing(input_text, base_date)\n\n        self.assertEqual(len(parser), 1)\n        self.assertEqual(parser[0][1].year, 2026)\n        self.assertEqual(parser[0][1].month, 2)\n        self.assertEqual(parser[0][1].day, 15)\n\n    def test_next_month_from_march_31st(self):\n        \"\"\"\n        Test 'next month' from March 31 handles April correctly\n        \"\"\"\n        base_date = datetime(2025, 3, 31, 12, 0, 0)\n        input_text = 'next month'\n        parser = parsing.datetime_parsing(input_text, base_date)\n\n        self.assertEqual(len(parser), 1)\n        # April only has 30 days, should pick the last valid day\n        self.assertEqual(parser[0][1].year, 2025)\n        self.assertEqual(parser[0][1].month, 4)\n        self.assertEqual(parser[0][1].day, 30)\n\n    def test_next_month_from_may_31st(self):\n        \"\"\"\n        Test 'next month' from May 31 handles June correctly\n        \"\"\"\n        base_date = datetime(2025, 5, 31, 12, 0, 0)\n        input_text = 'next month'\n        parser = parsing.datetime_parsing(input_text, base_date)\n\n        self.assertEqual(len(parser), 1)\n        # June only has 30 days, should pick the last valid day\n        self.assertEqual(parser[0][1].year, 2025)\n        self.assertEqual(parser[0][1].month, 6)\n        self.assertEqual(parser[0][1].day, 30)\n\n    def test_multiple_datetime_expressions(self):\n        \"\"\"\n        Test parsing text with multiple date/time references\n        \"\"\"\n        base_date = datetime(2025, 10, 18, 12, 0, 0)\n        input_text = 'Meeting today at 2pm and tomorrow at 3pm'\n        parser = parsing.datetime_parsing(input_text, base_date)\n\n        self.assertEqual(len(parser), 2)\n        # First: today at 2pm\n        self.assertEqual(parser[0][1].year, 2025)\n        self.assertEqual(parser[0][1].month, 10)\n        self.assertEqual(parser[0][1].day, 18)\n        self.assertEqual(parser[0][1].hour, 14)\n        # Second: tomorrow at 3pm\n        self.assertEqual(parser[1][1].year, 2025)\n        self.assertEqual(parser[1][1].month, 10)\n        self.assertEqual(parser[1][1].day, 19)\n        self.assertEqual(parser[1][1].hour, 15)\n\n    def test_duration_from_yesterday(self):\n        \"\"\"\n        Test '2 days after yesterday' using base_time\n        \"\"\"\n        base_date = datetime(2025, 10, 18, 12, 0, 0)\n        input_text = '2 days after yesterday'\n        parser = parsing.datetime_parsing(input_text, base_date)\n\n        self.assertEqual(len(parser), 1)\n        # Yesterday = Oct 17, + 2 days = Oct 19\n        self.assertEqual(parser[0][1].year, 2025)\n        self.assertEqual(parser[0][1].month, 10)\n        self.assertEqual(parser[0][1].day, 19)\n\n    def test_duration_from_tomorrow(self):\n        \"\"\"\n        Test '3 days after tomorrow'\n        \"\"\"\n        base_date = datetime(2025, 10, 18, 12, 0, 0)\n        input_text = '3 days after tomorrow'\n        parser = parsing.datetime_parsing(input_text, base_date)\n\n        self.assertEqual(len(parser), 1)\n        # Tomorrow = Oct 19, + 3 days = Oct 22\n        self.assertEqual(parser[0][1].year, 2025)\n        self.assertEqual(parser[0][1].month, 10)\n        self.assertEqual(parser[0][1].day, 22)\n\n    def test_duration_from_today(self):\n        \"\"\"\n        Test '5 days before today'\n        \"\"\"\n        base_date = datetime(2025, 10, 18, 12, 0, 0)\n        input_text = '5 days before today'\n        parser = parsing.datetime_parsing(input_text, base_date)\n\n        self.assertEqual(len(parser), 1)\n        # Today = Oct 18, - 5 days = Oct 13\n        self.assertEqual(parser[0][1].year, 2025)\n        self.assertEqual(parser[0][1].month, 10)\n        self.assertEqual(parser[0][1].day, 13)\n\n    def test_noon_without_convention(self):\n        \"\"\"\n        Test '12:00' without AM/PM defaults to AM convention (midnight = 0)\n        \"\"\"\n        base_date = datetime(2025, 10, 18, 0, 0, 0)\n        input_text = 'Meeting at 12:00'\n        parser = parsing.datetime_parsing(input_text, base_date)\n\n        self.assertEqual(len(parser), 1)\n        # No convention defaults to 'am', so 12:00 becomes 0 (midnight)\n        self.assertEqual(parser[0][1].hour, 0)\n        self.assertEqual(parser[0][1].minute, 0)\n\n    def test_twelve_pm(self):\n        \"\"\"\n        Test '12:00 pm' is noon (stays as 12)\n        \"\"\"\n        base_date = datetime(2025, 10, 18, 0, 0, 0)\n        input_text = 'Meeting at 12:00 pm'\n        parser = parsing.datetime_parsing(input_text, base_date)\n\n        self.assertEqual(len(parser), 1)\n        self.assertEqual(parser[0][1].hour, 12)\n        self.assertEqual(parser[0][1].minute, 0)\n\n    def test_twelve_am(self):\n        \"\"\"\n        Test '12:00 am' is midnight (converted to 0)\n        \"\"\"\n        base_date = datetime(2025, 10, 18, 0, 0, 0)\n        input_text = 'Meeting at 12:00 am'\n        parser = parsing.datetime_parsing(input_text, base_date)\n\n        self.assertEqual(len(parser), 1)\n        self.assertEqual(parser[0][1].hour, 0)\n        self.assertEqual(parser[0][1].minute, 0)\n\n    def test_one_am(self):\n        \"\"\"\n        Test '1:00 am' is 1:00\n        \"\"\"\n        base_date = datetime(2025, 10, 18, 0, 0, 0)\n        input_text = 'Meeting at 1:00 am'\n        parser = parsing.datetime_parsing(input_text, base_date)\n\n        self.assertEqual(len(parser), 1)\n        self.assertEqual(parser[0][1].hour, 1)\n        self.assertEqual(parser[0][1].minute, 0)\n\n    def test_one_pm(self):\n        \"\"\"\n        Test '1:00 pm' is 13:00\n        \"\"\"\n        base_date = datetime(2025, 10, 18, 0, 0, 0)\n        input_text = 'Meeting at 1:00 pm'\n        parser = parsing.datetime_parsing(input_text, base_date)\n\n        self.assertEqual(len(parser), 1)\n        self.assertEqual(parser[0][1].hour, 13)\n        self.assertEqual(parser[0][1].minute, 0)\n"
  },
  {
    "path": "tests/test_preprocessors.py",
    "content": "from tests.base_case import ChatBotTestCase\nfrom chatterbot.conversation import Statement\nfrom chatterbot import preprocessors\n\n\nclass PreprocessorIntegrationTestCase(ChatBotTestCase):\n    \"\"\"\n    Make sure that preprocessors work with the chat bot.\n    \"\"\"\n\n    def test_clean_whitespace(self):\n        self.chatbot.preprocessors = [preprocessors.clean_whitespace]\n        response = self.chatbot.get_response('Hello,    how are you?')\n\n        self.assertEqual(response.text, 'Hello, how are you?')\n\n\nclass CleanWhitespacePreprocessorTestCase(ChatBotTestCase):\n    \"\"\"\n    Make sure that ChatterBot's whitespace removing preprocessor works as expected.\n    \"\"\"\n\n    def test_clean_whitespace(self):\n        statement = Statement(text='\\tThe quick \\nbrown fox \\rjumps over \\vthe \\alazy \\fdog\\\\.')\n        cleaned = preprocessors.clean_whitespace(statement)\n        normal_text = 'The quick brown fox jumps over the \\alazy dog\\\\.'\n\n        self.assertEqual(cleaned.text, normal_text)\n\n    def test_leading_or_trailing_whitespace_removed(self):\n        statement = Statement(text='     The quick brown fox jumps over the lazy dog.   ')\n        cleaned = preprocessors.clean_whitespace(statement)\n        normal_text = 'The quick brown fox jumps over the lazy dog.'\n\n        self.assertEqual(cleaned.text, normal_text)\n\n    def test_consecutive_spaces_removed(self):\n        statement = Statement(text='The       quick brown     fox      jumps over the lazy dog.')\n        cleaned = preprocessors.clean_whitespace(statement)\n        normal_text = 'The quick brown fox jumps over the lazy dog.'\n\n        self.assertEqual(cleaned.text, normal_text)\n\n\nclass HTMLUnescapePreprocessorTestCase(ChatBotTestCase):\n    \"\"\"\n    Make sure that ChatterBot's html unescaping preprocessor works as expected.\n    \"\"\"\n\n    def test_html_unescape(self):\n\n        # implicit concatenation\n        statement = Statement(\n            text=(\n                'The quick brown fox &lt;b&gt;jumps&lt;/b&gt; over'\n                ' the <a href=\"http://lazy.com\">lazy</a> dog.'\n            )\n        )\n\n        normal_text = (\n            'The quick brown fox <b>jumps</b> over'\n            ' the <a href=\"http://lazy.com\">lazy</a> dog.'\n        )\n\n        cleaned = preprocessors.unescape_html(statement)\n\n        self.assertEqual(cleaned.text, normal_text)\n\n\nclass ConvertToASCIIPreprocessorTestCase(ChatBotTestCase):\n    \"\"\"\n    Make sure that ChatterBot's ASCII conversion preprocessor works as expected.\n    \"\"\"\n\n    def test_convert_to_ascii(self):\n        statement = Statement(text=u'Klüft skräms inför på fédéral électoral große')\n        cleaned = preprocessors.convert_to_ascii(statement)\n        normal_text = 'Kluft skrams infor pa federal electoral groe'\n\n        self.assertEqual(cleaned.text, normal_text)\n"
  },
  {
    "path": "tests/test_response_selection.py",
    "content": "from tests.base_case import ChatBotSQLTestCase\nfrom chatterbot import response_selection\nfrom chatterbot.conversation import Statement\n\n\nclass ResponseSelectionTests(ChatBotSQLTestCase):\n\n    def test_get_most_frequent_response(self):\n        statement_list = [\n            Statement(text='What... is your quest?', in_response_to='Hello'),\n            Statement(text='What... is your quest?', in_response_to='Hello'),\n            Statement(text='This is a phone.', in_response_to='Hello'),\n            Statement(text='This is a phone.', in_response_to='Hello'),\n            Statement(text='This is a phone.', in_response_to='Hello'),\n            Statement(text='This is a phone.', in_response_to='Hello'),\n            Statement(text='A what?', in_response_to='Hello'),\n            Statement(text='A what?', in_response_to='Hello'),\n            Statement(text='A phone.', in_response_to='Hello')\n        ]\n\n        for statement in statement_list:\n            self._create_with_search_text(\n                text=statement.text,\n                in_response_to=statement.in_response_to\n            )\n\n        output = response_selection.get_most_frequent_response(\n            Statement(text='Hello'),\n            statement_list,\n            self.chatbot.storage\n        )\n\n        self.assertEqual('This is a phone.', output.text)\n\n    def test_get_first_response(self):\n        statement_list = [\n            Statement(text='What... is your quest?'),\n            Statement(text='A what?'),\n            Statement(text='A quest.')\n        ]\n\n        output = response_selection.get_first_response(Statement(text='Hello'), statement_list)\n\n        self.assertEqual(output.text, 'What... is your quest?')\n\n    def test_get_random_response(self):\n        statement_list = [\n            Statement(text='This is a phone.'),\n            Statement(text='A what?'),\n            Statement(text='A phone.')\n        ]\n\n        output = response_selection.get_random_response(Statement(text='Hello'), statement_list)\n\n        self.assertTrue(output)\n"
  },
  {
    "path": "tests/test_search.py",
    "content": "from tests.base_case import ChatBotTestCase\nfrom chatterbot.conversation import Statement\nfrom chatterbot.search import TextSearch, IndexedTextSearch\nfrom chatterbot import comparisons\n\n\nclass SearchTestCase(ChatBotTestCase):\n\n    def setUp(self):\n        super().setUp()\n        self.search_algorithm = IndexedTextSearch(self.chatbot)\n\n    def test_search_no_results(self):\n        \"\"\"\n        An exception should be raised if there is no data to return.\n        \"\"\"\n        statement = Statement(text='What is your quest?')\n\n        with self.assertRaises(StopIteration):\n            next(self.search_algorithm.search(statement))\n\n    def test_search_cast_to_list_no_results(self):\n        \"\"\"\n        An empty list should be returned when the generator is\n        cast to a list and there are no results to return.\n        \"\"\"\n        statement = Statement(text='What is your quest?')\n\n        results = list(self.search_algorithm.search(statement))\n\n        self.assertEqual(results, [])\n\n    def test_search_additional_parameters(self):\n        \"\"\"\n        It should be possible to pass additional parameters in to use for searching.\n        \"\"\"\n        self._create_many_with_search_text([\n            Statement(text='B', in_response_to='A', conversation='test_1'),\n            Statement(text='B', in_response_to='A', conversation='test_2')\n        ])\n\n        statement = Statement(text='A')\n\n        results = list(self.search_algorithm.search(\n            statement, conversation='test_1'\n        ))\n\n        self.assertIsLength(results, 1)\n        self.assertEqual(results[0].text, 'B')\n        self.assertEqual(results[0].conversation, 'test_1')\n\n\nclass IndexedTextSearchComparisonFunctionSpacySimilarityTests(ChatBotTestCase):\n    \"\"\"\n    Test that the search algorithm works correctly with the\n    spacy similarity comparison function.\n    \"\"\"\n\n    def setUp(self):\n        super().setUp()\n        self.search_algorithm = IndexedTextSearch(\n            self.chatbot,\n            statement_comparison_function=comparisons.SpacySimilarity\n        )\n\n    def test_get_closest_statement(self):\n        \"\"\"\n        Note, the content of the in_response_to field for each of the\n        test statements is only required because the logic adapter will\n        filter out any statements that are not in response to a known statement.\n        \"\"\"\n        self._create_many_with_search_text([\n            Statement(text='This is a lovely bog.', in_response_to='This is a lovely bog.'),\n            Statement(text='This is a beautiful swamp.', in_response_to='This is a beautiful swamp.'),\n            Statement(text='It smells like a swamp.', in_response_to='It smells like a swamp.')\n        ])\n\n        statement = Statement(text='This is a lovely swamp.')\n        results = list(self.search_algorithm.search(statement))\n\n        self.assertIsLength(results, 2)\n        self.assertEqual(results[1].text, 'This is a beautiful swamp.')\n        self.assertGreater(results[1].confidence, 0)\n\n    def test_different_punctuation(self):\n        self._create_many_with_search_text([\n            Statement(text='A', in_response_to='Who are you?'),\n            Statement(text='B', in_response_to='Are you good?'),\n            Statement(text='C', in_response_to='You are good')\n        ])\n\n        statement = Statement(text='Are you good')\n        results = list(self.search_algorithm.search(statement))\n\n        self.assertEqual(len(results), 2, msg=[r.search_text for r in results])\n        # Note: the last statement in the list always has the highest confidence\n        self.assertEqual(results[-1].text, 'B')\n\n\nclass IndexedTextSearchComparisonFunctionLevenshteinDistanceComparisonTests(ChatBotTestCase):\n    \"\"\"\n    Test that the search algorithm works correctly with the\n    Levenshtein distance comparison function.\n    \"\"\"\n\n    def setUp(self):\n        super().setUp()\n        self.search_algorithm = IndexedTextSearch(\n            self.chatbot,\n            statement_comparison_function=comparisons.LevenshteinDistance\n        )\n\n    def test_get_closest_statement(self):\n        \"\"\"\n        Note, the content of the in_response_to field for each of the\n        test statements is only required because the search process will\n        filter out any statements that are not in response to something.\n        \"\"\"\n        self._create_many_with_search_text([\n            Statement(text='A', in_response_to='What is the meaning of life?'),\n            Statement(text='B', in_response_to='I am Iron Man.'),\n            Statement(text='C', in_response_to='What... is your quest?'),\n            Statement(text='D', in_response_to='Yuck, black licorice jelly beans.'),\n            Statement(text='E', in_response_to='I hear you are going on a quest?'),\n        ])\n\n        statement = Statement(text='What is your quest?')\n\n        results = list(self.search_algorithm.search(statement))\n\n        self.assertEqual(len(results), 2, msg=[r.in_response_to for r in results])\n        self.assertEqual(results[1].in_response_to, 'What... is your quest?')\n\n    def test_confidence_exact_match(self):\n        self._create_with_search_text(text='What is your quest?', in_response_to='What is your quest?')\n\n        statement = Statement(text='What is your quest?')\n        results = list(self.search_algorithm.search(statement))\n\n        self.assertIsLength(results, 1)\n        self.assertEqual(results[0].confidence, 1)\n\n    def test_confidence_half_match(self):\n        from unittest.mock import MagicMock\n\n        # Assume that the storage adapter returns a partial match\n        self.chatbot.storage.filter = MagicMock(return_value=[\n            Statement(text='', in_response_to='xxyy')\n        ])\n\n        statement = Statement(text='wwxx')\n        results = list(self.search_algorithm.search(statement))\n\n        self.assertIsLength(results, 1)\n        self.assertEqual(results[0].confidence, 0.5, msg=results)\n\n    def test_confidence_no_match(self):\n        from unittest.mock import MagicMock\n\n        # Assume that the storage adapter returns a partial match\n        self.search_algorithm.chatbot.storage.filter = MagicMock(return_value=[\n            Statement(text='xxx', in_response_to='xxx')\n        ])\n\n        statement = Statement(text='yyy')\n        results = list(self.search_algorithm.search(statement))\n\n        self.assertIsLength(results, 0)\n\n\nclass TextSearchComparisonFunctionSpacySimilarityTests(ChatBotTestCase):\n    \"\"\"\n    Test that the search algorithm works correctly with the\n    spacy similarity comparison function.\n    \"\"\"\n\n    def setUp(self):\n        super().setUp()\n        self.search_algorithm = TextSearch(\n            self.chatbot,\n            statement_comparison_function=comparisons.SpacySimilarity\n        )\n\n    def test_get_closest_statement(self):\n        \"\"\"\n        Note, the content of the in_response_to field for each of the\n        test statements is only required because the logic adapter will\n        filter out any statements that are not in response to a known statement.\n        \"\"\"\n        self._create_many_with_search_text([\n            Statement(text='This is a lovely bog.', in_response_to='This is a lovely bog.'),\n            Statement(text='This is a beautiful swamp.', in_response_to='This is a beautiful swamp.'),\n            Statement(text='It smells like a swamp.', in_response_to='It smells like a swamp.')\n        ])\n\n        statement = Statement(text='This is a lovely swamp.')\n        results = list(self.search_algorithm.search(statement))\n\n        self.assertIsLength(results, 2)\n        self.assertEqual(results[-1].text, 'This is a beautiful swamp.')\n        self.assertGreater(results[-1].confidence, 0)\n\n    def test_different_punctuation(self):\n        self._create_many_with_search_text([\n            Statement(text='A', in_response_to='Who are you?'),\n            Statement(text='B', in_response_to='Are you good?'),\n            Statement(text='C', in_response_to='You are good')\n        ])\n\n        statement = Statement(text='Are you good')\n        results = list(self.search_algorithm.search(statement))\n\n        self.assertEqual(len(results), 2)\n        # Note: the last statement in the list always has the highest confidence\n        self.assertEqual(results[-1].in_response_to, 'Are you good?')\n\n\nclass TextSearchComparisonFunctionLevenshteinDistanceComparisonTests(ChatBotTestCase):\n    \"\"\"\n    Test that the search algorithm works correctly with the\n    Levenshtein distance comparison function.\n    \"\"\"\n\n    def setUp(self):\n        super().setUp()\n        self.search_algorithm = TextSearch(\n            self.chatbot,\n            statement_comparison_function=comparisons.LevenshteinDistance\n        )\n\n    def test_get_closest_statement(self):\n        \"\"\"\n        Note, the content of the in_response_to field for each of the\n        test statements is only required because the search process will\n        filter out any statements that are not in response to something.\n        \"\"\"\n        self._create_many_with_search_text([\n            Statement(text='A', in_response_to='What is the meaning of life?'),\n            Statement(text='B', in_response_to='I am Iron Man.'),\n            Statement(text='C', in_response_to='What... is your quest?'),\n            Statement(text='D', in_response_to='Yuck, black licorice jelly beans.'),\n            Statement(text='E', in_response_to='I hear you are going on a quest?'),\n        ])\n\n        statement = Statement(text='What is your quest?')\n\n        results = list(self.search_algorithm.search(statement))\n\n        self.assertEqual(len(results), 2)\n        self.assertEqual(results[-1].in_response_to, 'What... is your quest?', msg=results[-1].confidence)\n\n    def test_confidence_exact_match(self):\n        self._create_with_search_text(text='What is your quest?', in_response_to='What is your quest?')\n\n        statement = Statement(text='What is your quest?')\n        results = list(self.search_algorithm.search(statement))\n\n        self.assertIsLength(results, 1)\n        self.assertEqual(results[0].confidence, 1)\n\n    def test_confidence_half_match(self):\n        from unittest.mock import MagicMock\n\n        # Assume that the storage adapter returns a partial match\n        self.chatbot.storage.filter = MagicMock(return_value=[\n            Statement(text='', in_response_to='xxyy')\n        ])\n\n        statement = Statement(text='wwxx')\n        results = list(self.search_algorithm.search(statement))\n\n        self.assertIsLength(results, 1)\n        self.assertEqual(results[0].confidence, 0.5)\n\n    def test_confidence_no_match(self):\n        from unittest.mock import MagicMock\n\n        # Assume that the storage adapter returns a partial match\n        self.search_algorithm.chatbot.storage.filter = MagicMock(return_value=[\n            Statement(text='xxx', in_response_to='xxx')\n        ])\n\n        statement = Statement(text='yyy')\n        results = list(self.search_algorithm.search(statement))\n\n        self.assertIsLength(results, 0)\n"
  },
  {
    "path": "tests/test_tagging.py",
    "content": "from unittest import TestCase\nfrom chatterbot import languages\nfrom chatterbot import tagging\n\n\nclass PosLemmaTaggerTests(TestCase):\n\n    def setUp(self):\n        self.tagger = tagging.PosLemmaTagger()\n\n    def test_empty_string(self):\n        tagged_text = self.tagger.get_text_index_string(\n            ''\n        )\n\n        self.assertEqual(tagged_text, '')\n\n    def test_tagging(self):\n        tagged_text = self.tagger.get_text_index_string(\n            'Hello, how are you doing on this awesome day?'\n        )\n\n        self.assertEqual(tagged_text, 'INTJ:awesome ADJ:day')\n\n    def test_tagging_english(self):\n        self.tagger = tagging.PosLemmaTagger(\n            language=languages.ENG\n        )\n\n        tagged_text = self.tagger.get_text_index_string(\n            'Hello, how are you doing on this awesome day?'\n        )\n\n        self.assertEqual(tagged_text, 'INTJ:awesome ADJ:day')\n\n    def test_tagging_german(self):\n        self.tagger = tagging.PosLemmaTagger(\n            language=languages.GER\n        )\n\n        tagged_text = self.tagger.get_text_index_string(\n            'Ich spreche nicht viel Deutsch.'\n        )\n\n        self.assertEqual(tagged_text, 'VERB:deutsch')\n\n    def test_string_becomes_lowercase(self):\n        tagged_text = self.tagger.get_text_index_string('THIS IS HOW IT BEGINS!')\n\n        self.assertEqual(tagged_text, 'PRON:be AUX:how SCONJ:it PRON:begin')\n\n    def test_tagging_medium_sized_words(self):\n        tagged_text = self.tagger.get_text_index_string('Hello, my name is Gunther.')\n\n        self.assertEqual(tagged_text, 'INTJ:gunther')\n\n    def test_tagging_long_words(self):\n        tagged_text = self.tagger.get_text_index_string('I play several orchestra instruments for pleasure.')\n\n        self.assertEqual(tagged_text, 'VERB:orchestra NOUN:instrument NOUN:pleasure')\n\n    def test_get_text_index_string_punctuation_only(self):\n        bigram_string = self.tagger.get_text_index_string(\n            '?'\n        )\n\n        self.assertEqual(bigram_string, '?')\n\n    def test_get_text_index_string_single_character(self):\n        bigram_string = self.tagger.get_text_index_string(\n            '🙂'\n        )\n\n        self.assertEqual(bigram_string, '🙂')\n\n    def test_get_text_index_string_single_character_punctuated(self):\n        bigram_string = self.tagger.get_text_index_string(\n            '🤷?'\n        )\n\n        self.assertEqual(bigram_string, '🤷')\n\n    def test_get_text_index_string_two_characters(self):\n        bigram_string = self.tagger.get_text_index_string(\n            'AB'\n        )\n\n        self.assertEqual(bigram_string, 'ab')\n\n    def test_get_text_index_string_three_characters(self):\n        bigram_string = self.tagger.get_text_index_string(\n            'ABC'\n        )\n\n        self.assertEqual(bigram_string, 'abc')\n\n    def test_get_text_index_string_four_characters(self):\n        bigram_string = self.tagger.get_text_index_string(\n            'ABCD'\n        )\n\n        self.assertEqual(bigram_string, 'abcd')\n\n    def test_get_text_index_string_five_characters(self):\n        bigram_string = self.tagger.get_text_index_string(\n            'ABCDE'\n        )\n\n        self.assertEqual(bigram_string, 'abcde')\n\n    def test_get_text_index_string_single_word(self):\n        bigram_string = self.tagger.get_text_index_string(\n            'Hello'\n        )\n\n        self.assertEqual(bigram_string, 'hello')\n\n    def test_get_text_index_string_multiple_words(self):\n        bigram_string = self.tagger.get_text_index_string(\n            'Hello Dr. Salazar. How are you today?'\n        )\n\n        self.assertEqual(bigram_string, 'INTJ:dr. PROPN:salazar PROPN:today')\n\n    def test_get_text_index_string_single_character_words(self):\n        bigram_string = self.tagger.get_text_index_string(\n            'a e i o u'\n        )\n\n        self.assertEqual(bigram_string, 'NOUN:o NOUN:u')\n\n    def test_get_text_index_string_two_character_words(self):\n        bigram_string = self.tagger.get_text_index_string(\n            'Lo my mu it is of us'\n        )\n\n        self.assertEqual(bigram_string, 'VERB:mu')\n\n\nclass LowercaseTaggerTests(TestCase):\n\n    def setUp(self):\n        self.tagger = tagging.LowercaseTagger()\n\n    def test_lowercase_tagger(self):\n        tagged_text = self.tagger.get_text_index_string(\n            'Hello, how are you doing on this AWESOME day?'\n        )\n\n        self.assertEqual(tagged_text, 'hello, how are you doing on this awesome day?')\n"
  },
  {
    "path": "tests/test_turing.py",
    "content": "from unittest import TestCase, expectedFailure\n\n\nclass TuringTests(TestCase):\n\n    def setUp(self):\n        from chatterbot import ChatBot\n\n        self.chatbot = ChatBot('Agent Jr.')\n\n    @expectedFailure\n    def test_ask_name(self):\n        response = self.chatbot.get_response(\n            'What is your name?'\n        )\n        self.assertIn('Agent', response.text)\n\n    @expectedFailure\n    def test_repeat_information(self):\n        \"\"\"\n        Test if we can detect any repeat responses from the agent.\n        \"\"\"\n        self.fail('Condition not met.')\n\n    @expectedFailure\n    def test_repeat_input(self):\n        \"\"\"\n        Test what the responses are like if we keep giving the same input.\n        \"\"\"\n        self.fail('Condition not met.')\n\n    @expectedFailure\n    def test_contradicting_responses(self):\n        \"\"\"\n        Test if we can get the agent to contradict themselves.\n        \"\"\"\n        self.fail('Condition not met.')\n\n    @expectedFailure\n    def test_mathematical_ability(self):\n        \"\"\"\n        The math questions inherently suggest that the agent\n        should get some math problems wrong in order to seem\n        more human. My view on this is that it is more useful\n        to have a bot that is good at math, which could just\n        as easily be a human.\n        \"\"\"\n        self.fail('Condition not met.')\n\n    @expectedFailure\n    def test_response_time(self):\n        \"\"\"\n        Does the agent respond in a realistic amount of time?\n        \"\"\"\n        self.fail('Condition not met.')\n"
  },
  {
    "path": "tests/test_utils.py",
    "content": "from tests.base_case import ChatBotTestCase\nfrom unittest import TestCase\nfrom chatterbot import utils\n\n\nclass UtilityTests(TestCase):\n\n    def test_import_module(self):\n        datetime = utils.import_module('datetime.datetime')\n        self.assertTrue(hasattr(datetime, 'now'))\n\n\nclass UtilityChatBotTestCase(ChatBotTestCase):\n\n    def test_get_response_time(self):\n        \"\"\"\n        Test that a response time is returned.\n        \"\"\"\n\n        response_time = utils.get_response_time(self.chatbot)\n\n        self.assertGreater(response_time, 0)\n"
  },
  {
    "path": "tests/training/__init__.py",
    "content": ""
  },
  {
    "path": "tests/training/test_chatterbot_corpus_training.py",
    "content": "from tests.base_case import ChatBotTestCase\nfrom chatterbot.trainers import ChatterBotCorpusTrainer\n\n\nclass ChatterBotCorpusTrainingTestCase(ChatBotTestCase):\n    \"\"\"\n    Test case for training with data from the ChatterBot Corpus.\n\n    Note: This class has a mirror tests_django/integration_tests/\n    \"\"\"\n\n    def setUp(self):\n        super().setUp()\n        self.trainer = ChatterBotCorpusTrainer(\n            self.chatbot,\n            show_training_progress=False\n        )\n\n    def test_train_with_english_greeting_corpus(self):\n        self.trainer.train('chatterbot.corpus.english.greetings')\n\n        results = list(self.chatbot.storage.filter(text='Hello'))\n\n        self.assertGreater(len(results), 1)\n\n    def test_train_with_english_greeting_corpus_search_text(self):\n        self.trainer.train('chatterbot.corpus.english.greetings')\n\n        results = list(self.chatbot.storage.filter(text='Hello'))\n\n        self.assertGreater(len(results), 1)\n        self.assertEqual(results[0].search_text, 'hello')\n\n    def test_train_with_english_greeting_corpus_search_in_response_to(self):\n        self.trainer.train('chatterbot.corpus.english.greetings')\n\n        results = list(self.chatbot.storage.filter(in_response_to='Hello'))\n\n        self.assertGreater(len(results), 1)\n        self.assertEqual(results[0].search_in_response_to, 'hello')\n\n    def test_train_with_english_greeting_corpus_tags(self):\n        self.trainer.train('chatterbot.corpus.english.greetings')\n\n        results = list(self.chatbot.storage.filter(text='Hello'))\n\n        self.assertGreater(len(results), 1)\n        statement = results[0]\n        self.assertEqual(['greetings'], statement.get_tags())\n\n    def test_train_with_multiple_corpora(self):\n        self.trainer.train(\n            'chatterbot.corpus.english.greetings',\n            'chatterbot.corpus.english.conversations',\n        )\n        results = list(self.chatbot.storage.filter(text='Hello'))\n\n        self.assertGreater(len(results), 1)\n\n    def test_train_with_english_corpus(self):\n        self.trainer.train('chatterbot.corpus.english')\n        results = list(self.chatbot.storage.filter(text='Hello'))\n\n        self.assertGreater(len(results), 1)\n"
  },
  {
    "path": "tests/training/test_csv_file_training.py",
    "content": "import os\nfrom tests.base_case import ChatBotTestCase\nfrom chatterbot.trainers import CsvFileTrainer\n\n\nclass CsvFileTrainerTestCase(ChatBotTestCase):\n    \"\"\"\n    Test training from CSV files.\n    \"\"\"\n\n    def setUp(self):\n        super().setUp()\n\n        current_directory = os.path.dirname(os.path.abspath(__file__))\n\n        self.data_file_path = os.path.join(\n            current_directory,\n            'test_data/csv_corpus/'\n        )\n\n        self.trainer = CsvFileTrainer(\n            self.chatbot,\n            show_training_progress=False,\n            field_map={\n                'created_at': 0,\n                'persona': 1,\n                'text': 2,\n                'conversation': 3\n            }\n        )\n\n    def test_train(self):\n        \"\"\"\n        Test that the chat bot is trained using data from the CSV files.\n        \"\"\"\n        self.trainer.train(self.data_file_path)\n\n        response = self.chatbot.get_response('Is anyone there?')\n        self.assertEqual(response.text, 'Yes')\n\n    def test_train_sets_search_text(self):\n        \"\"\"\n        Test that the chat bot is trained using data from the CSV files.\n        \"\"\"\n        self.trainer.train(self.data_file_path)\n\n        results = list(self.chatbot.storage.filter(text='Is anyone there?'))\n\n        self.assertEqual(len(results), 2, msg='Results: {}'.format(results))\n        self.assertEqual(results[0].search_text, 'AUX:anyone PRON:there')\n\n    def test_train_sets_search_in_response_to(self):\n        \"\"\"\n        Test that the chat bot is trained using data from the CSV files.\n        \"\"\"\n        self.trainer.train(self.data_file_path)\n\n        results = list(self.chatbot.storage.filter(in_response_to='Is anyone there?'))\n\n        self.assertEqual(len(results), 2)\n        self.assertEqual(results[0].search_in_response_to, 'AUX:anyone PRON:there')\n"
  },
  {
    "path": "tests/training/test_data/csv_corpus/1.csv",
    "content": "2004-11-04T16:49:00.000Z,tom,Hello,testing\n2004-11-04T16:49:00.000Z,tom,Is anyone there?,testing\n2004-11-04T16:49:00.000Z,jane,Yes,testing\n"
  },
  {
    "path": "tests/training/test_data/csv_corpus/2.csv",
    "content": "2004-11-04T16:49:00.000Z,tom,Hello,testing-2\n2004-11-04T16:49:00.000Z,tom,Is anyone there?,testing-2\n2004-11-04T16:49:00.000Z,jane,Yes,testing-2\n"
  },
  {
    "path": "tests/training/test_data/get_search.json",
    "content": "{\"statuses\":[{\"metadata\":{\"result_type\":\"popular\",\"iso_language_code\":\"en\"},\"created_at\":\"Tue Dec 08 21:40:00 +0000 2015\",\"id\":674342688083283970,\"id_str\":\"674342688083283970\",\"text\":\"\\ud83c\\udfb6 C++, Java, Python &amp; Ruby. These are a few of my favorite things \\ud83c\\udfb6 #HourOfCode \\ud83d\\udd51\\ud83d\\udcbb\\ud83d\\udc7e\\ud83c\\udfae https:\\/\\/t.co\\/GSCmPh9V6j\",\"source\":\"\\u003ca href=\\\"https:\\/\\/vine.co\\\" rel=\\\"nofollow\\\"\\u003eVine for Android\\u003c\\/a\\u003e\",\"truncated\":false,\"in_reply_to_status_id\":null,\"in_reply_to_status_id_str\":null,\"in_reply_to_user_id\":null,\"in_reply_to_user_id_str\":null,\"in_reply_to_screen_name\":null,\"user\":{\"id\":58309829,\"id_str\":\"58309829\",\"name\":\"Nickelodeon\",\"screen_name\":\"NickelodeonTV\",\"location\":\"USA\",\"description\":\"The Official Twitter for Nickelodeon, USA!\",\"url\":\"https:\\/\\/t.co\\/Lz9i6LdC4f\",\"entities\":{\"url\":{\"urls\":[{\"url\":\"https:\\/\\/t.co\\/Lz9i6LdC4f\",\"expanded_url\":\"http:\\/\\/www.nick.com\",\"display_url\":\"nick.com\",\"indices\":[0,23]}]},\"description\":{\"urls\":[]}},\"protected\":false,\"followers_count\":3914587,\"friends_count\":2263,\"listed_count\":3321,\"created_at\":\"Sun Jul 19 22:19:02 +0000 2009\",\"favourites_count\":2757,\"utc_offset\":-18000,\"time_zone\":\"Eastern Time (US & Canada)\",\"geo_enabled\":true,\"verified\":true,\"statuses_count\":33910,\"lang\":\"en\",\"contributors_enabled\":false,\"is_translator\":false,\"is_translation_enabled\":true,\"profile_background_color\":\"FA743E\",\"profile_background_image_url\":\"http:\\/\\/pbs.twimg.com\\/profile_background_images\\/450718163508789248\\/E26KBqrx.jpeg\",\"profile_background_image_url_https\":\"https:\\/\\/pbs.twimg.com\\/profile_background_images\\/450718163508789248\\/E26KBqrx.jpeg\",\"profile_background_tile\":false,\"profile_image_url\":\"http:\\/\\/pbs.twimg.com\\/profile_images\\/671387650792665088\\/sJxvItMD_normal.jpg\",\"profile_image_url_https\":\"https:\\/\\/pbs.twimg.com\\/profile_images\\/671387650792665088\\/sJxvItMD_normal.jpg\",\"profile_banner_url\":\"https:\\/\\/pbs.twimg.com\\/profile_banners\\/58309829\\/1448906254\",\"profile_link_color\":\"D1771E\",\"profile_sidebar_border_color\":\"FFFFFF\",\"profile_sidebar_fill_color\":\"F0F0F0\",\"profile_text_color\":\"333333\",\"profile_use_background_image\":false,\"has_extended_profile\":false,\"default_profile\":false,\"default_profile_image\":false,\"following\":false,\"follow_request_sent\":false,\"notifications\":false},\"geo\":null,\"coordinates\":null,\"place\":null,\"contributors\":null,\"is_quote_status\":false,\"retweet_count\":28,\"favorite_count\":126,\"entities\":{\"hashtags\":[{\"text\":\"HourOfCode\",\"indices\":[72,83]}],\"symbols\":[],\"user_mentions\":[],\"urls\":[{\"url\":\"https:\\/\\/t.co\\/GSCmPh9V6j\",\"expanded_url\":\"https:\\/\\/vine.co\\/v\\/i7QJji9Ldmr\",\"display_url\":\"vine.co\\/v\\/i7QJji9Ldmr\",\"indices\":[89,112]}]},\"favorited\":false,\"retweeted\":false,\"possibly_sensitive\":false,\"lang\":\"en\"},{\"metadata\":{\"result_type\":\"popular\",\"iso_language_code\":\"en\"},\"created_at\":\"Tue Dec 08 21:45:00 +0000 2015\",\"id\":674342688083283970,\"id_str\":\"674342688083283970\",\"text\":\"Are you sure about Ruby?\",\"source\":\"\\u003ca href=\\\"https:\\/\\/vine.co\\\" rel=\\\"nofollow\\\"\\u003eVine for Android\\u003c\\/a\\u003e\",\"truncated\":false,\"in_reply_to_status_id\":674342688083283970,\"in_reply_to_status_id_str\":\"674342688083283970\",\"in_reply_to_user_id\":null,\"in_reply_to_user_id_str\":null,\"in_reply_to_screen_name\":null,\"user\":{\"id\":58309829,\"id_str\":\"58309829\",\"name\":\"Nickelodeon\",\"screen_name\":\"NickelodeonTV\",\"location\":\"USA\",\"description\":\"The Official Twitter for Nickelodeon, USA!\",\"url\":\"https:\\/\\/t.co\\/Lz9i6LdC4f\",\"entities\":{\"url\":{\"urls\":[{\"url\":\"https:\\/\\/t.co\\/Lz9i6LdC4f\",\"expanded_url\":\"http:\\/\\/www.nick.com\",\"display_url\":\"nick.com\",\"indices\":[0,23]}]},\"description\":{\"urls\":[]}},\"protected\":false,\"followers_count\":3914587,\"friends_count\":2263,\"listed_count\":3321,\"created_at\":\"Sun Jul 19 22:19:02 +0000 2009\",\"favourites_count\":2757,\"utc_offset\":-18000,\"time_zone\":\"Eastern Time (US & Canada)\",\"geo_enabled\":true,\"verified\":true,\"statuses_count\":33910,\"lang\":\"en\",\"contributors_enabled\":false,\"is_translator\":false,\"is_translation_enabled\":true,\"profile_background_color\":\"FA743E\",\"profile_background_image_url\":\"http:\\/\\/pbs.twimg.com\\/profile_background_images\\/450718163508789248\\/E26KBqrx.jpeg\",\"profile_background_image_url_https\":\"https:\\/\\/pbs.twimg.com\\/profile_background_images\\/450718163508789248\\/E26KBqrx.jpeg\",\"profile_background_tile\":false,\"profile_image_url\":\"http:\\/\\/pbs.twimg.com\\/profile_images\\/671387650792665088\\/sJxvItMD_normal.jpg\",\"profile_image_url_https\":\"https:\\/\\/pbs.twimg.com\\/profile_images\\/671387650792665088\\/sJxvItMD_normal.jpg\",\"profile_banner_url\":\"https:\\/\\/pbs.twimg.com\\/profile_banners\\/58309829\\/1448906254\",\"profile_link_color\":\"D1771E\",\"profile_sidebar_border_color\":\"FFFFFF\",\"profile_sidebar_fill_color\":\"F0F0F0\",\"profile_text_color\":\"333333\",\"profile_use_background_image\":false,\"has_extended_profile\":false,\"default_profile\":false,\"default_profile_image\":false,\"following\":false,\"follow_request_sent\":false,\"notifications\":false},\"geo\":null,\"coordinates\":null,\"place\":null,\"contributors\":null,\"is_quote_status\":false,\"retweet_count\":28,\"favorite_count\":126,\"entities\":{\"hashtags\":[{\"text\":\"HourOfCode\",\"indices\":[72,83]}],\"symbols\":[],\"user_mentions\":[],\"urls\":[{\"url\":\"https:\\/\\/t.co\\/GSCmPh9V6j\",\"expanded_url\":\"https:\\/\\/vine.co\\/v\\/i7QJji9Ldmr\",\"display_url\":\"vine.co\\/v\\/i7QJji9Ldmr\",\"indices\":[89,112]}]},\"favorited\":false,\"retweeted\":false,\"possibly_sensitive\":false,\"lang\":\"en\"}]}"
  },
  {
    "path": "tests/training/test_data/json_corpus/1.json",
    "content": "{\n    \"conversation\": [\n        {\n            \"text\": \"Hello\",\n            \"in_response_to\": null,\n            \"persona\": \"user 1\",\n            \"conversation\": \"test\",\n            \"tags\": [\n                \"greeting\"\n            ]\n        },\n        {\n            \"text\": \"Is anyone there?\",\n            \"in_response_to\": \"Hello\",\n            \"persona\": \"user 1\",\n            \"conversation\": \"test\",\n            \"tags\": [\n                \"question\"\n            ]\n        },\n        {\n            \"text\": \"Yes\",\n            \"in_response_to\": \"Is anyone there?\",\n            \"persona\": \"user 2\",\n            \"conversation\": \"test\"\n        }\n    ]\n}"
  },
  {
    "path": "tests/training/test_data/json_corpus/2.json",
    "content": "{\n    \"conversation\": [\n        {\n            \"text\": \"Hello\",\n            \"in_response_to\": null,\n            \"persona\": \"user 1\",\n            \"conversation\": \"test-2\"\n        },\n        {\n            \"text\": \"Is anyone there?\",\n            \"in_response_to\": \"Hello\",\n            \"persona\": \"user 1\",\n            \"conversation\": \"test-2\"\n        },\n        {\n            \"text\": \"Yes\",\n            \"in_response_to\": \"Is anyone there?\",\n            \"persona\": \"user 2\",\n            \"conversation\": \"test-2\"\n        }\n    ]\n}"
  },
  {
    "path": "tests/training/test_json_file_training.py",
    "content": "import os\nfrom tests.base_case import ChatBotTestCase\nfrom chatterbot.trainers import JsonFileTrainer\n\n\nclass JsonFileTrainerTestCase(ChatBotTestCase):\n    \"\"\"\n    Test training from JSON files.\n    \"\"\"\n\n    def setUp(self):\n        super().setUp()\n\n        current_directory = os.path.dirname(os.path.abspath(__file__))\n\n        self.data_file_path = os.path.join(\n            current_directory,\n            'test_data/json_corpus/'\n        )\n\n        self.trainer = JsonFileTrainer(\n            self.chatbot,\n            show_training_progress=False,\n            field_map={\n                'persona': 'persona',\n                'text': 'text',\n                'conversation': 'conversation',\n                'in_response_to': 'in_response_to',\n            }\n        )\n\n    def test_train(self):\n        \"\"\"\n        Test that the chat bot is trained using data from the JSON files.\n        \"\"\"\n        self.trainer.train(self.data_file_path)\n\n        response = self.chatbot.get_response('Is anyone there?')\n        self.assertEqual(response.text, 'Yes')\n\n    def test_train_sets_search_text(self):\n        \"\"\"\n        Test that the chat bot is trained using data from the JSON files.\n        \"\"\"\n        self.trainer.train(self.data_file_path)\n\n        results = list(self.chatbot.storage.filter(text='Is anyone there?'))\n\n        self.assertEqual(len(results), 2, msg='Results: {}'.format(results))\n        self.assertEqual(results[0].search_text, 'AUX:anyone PRON:there')\n\n    def test_train_sets_search_in_response_to(self):\n        \"\"\"\n        Test that the chat bot is trained using data from the JSON files.\n        \"\"\"\n        self.trainer.train(self.data_file_path)\n\n        results = list(self.chatbot.storage.filter(in_response_to='Is anyone there?'))\n\n        self.assertEqual(len(results), 2)\n        self.assertEqual(results[0].search_in_response_to, 'AUX:anyone PRON:there')\n"
  },
  {
    "path": "tests/training/test_list_training.py",
    "content": "from tests.base_case import ChatBotTestCase\nfrom chatterbot.trainers import ListTrainer\nfrom chatterbot import preprocessors\n\n\nclass ListTrainingTests(ChatBotTestCase):\n\n    def setUp(self):\n        super().setUp()\n        self.trainer = ListTrainer(\n            self.chatbot,\n            show_training_progress=False\n        )\n\n    def test_training_cleans_whitespace(self):\n        \"\"\"\n        Test that the ``clean_whitespace`` preprocessor is used during\n        the training process.\n        \"\"\"\n        self.chatbot.preprocessors = [preprocessors.clean_whitespace]\n\n        self.trainer.train([\n            'Can I help you with anything?',\n            'No, I     think I am all set.',\n            'Okay, have a nice day.',\n            'Thank you, you too.'\n        ])\n\n        response = self.chatbot.get_response('Can I help you with anything?')\n\n        self.assertEqual(response.text, 'No, I think I am all set.')\n\n    def test_training_adds_statements(self):\n        \"\"\"\n        Test that the training method adds statements\n        to the database.\n        \"\"\"\n        conversation = [\n            \"Hello\",\n            \"Hi there!\",\n            \"How are you doing?\",\n            \"I'm great.\",\n            \"That is good to hear\",\n            \"Thank you.\",\n            \"You are welcome.\",\n            \"Sure, any time.\",\n            \"Yeah\",\n            \"Can I help you with anything?\"\n        ]\n\n        self.trainer.train(conversation)\n\n        response = self.chatbot.get_response(\"Thank you.\")\n\n        self.assertEqual(response.text, \"You are welcome.\")\n\n    def test_training_sets_in_response_to(self):\n\n        conversation = [\n            \"Do you like my hat?\",\n            \"I do not like your hat.\"\n        ]\n\n        self.trainer.train(conversation)\n\n        statements = list(self.chatbot.storage.filter(\n            in_response_to=\"Do you like my hat?\"\n        ))\n\n        self.assertIsLength(statements, 1)\n        self.assertEqual(statements[0].in_response_to, \"Do you like my hat?\")\n\n    def test_training_sets_search_text(self):\n\n        conversation = [\n            \"Do you like my hat?\",\n            \"I do not like your hat.\"\n        ]\n\n        self.trainer.train(conversation)\n\n        statements = list(self.chatbot.storage.filter(\n            in_response_to=\"Do you like my hat?\"\n        ))\n\n        self.assertIsLength(statements, 1)\n        self.assertEqual(statements[0].search_text, 'VERB:hat')\n\n    def test_training_sets_search_in_response_to(self):\n\n        conversation = [\n            \"Do you like my hat?\",\n            \"I do not like your hat.\"\n        ]\n\n        self.trainer.train(conversation)\n\n        statements = list(self.chatbot.storage.filter(\n            in_response_to=\"Do you like my hat?\"\n        ))\n\n        self.assertIsLength(statements, 1)\n        self.assertEqual(statements[0].search_in_response_to, 'VERB:hat')\n\n    def test_database_has_correct_format(self):\n        \"\"\"\n        Test that the database maintains a valid format\n        when data is added and updated. This means that\n        after the training process, the database should\n        contain nine objects and eight of these objects\n        should list the previous member of the list as\n        a response.\n        \"\"\"\n        conversation = [\n            \"Hello sir!\",\n            \"Hi, can I help you?\",\n            \"Yes, I am looking for italian parsely.\",\n            \"Italian parsely is right over here in out produce department\",\n            \"Great, thank you for your help.\",\n            \"No problem, did you need help finding anything else?\",\n            \"Nope, that was it.\",\n            \"Alright, have a great day.\",\n            \"Thanks, you too.\"\n        ]\n\n        self.trainer.train(conversation)\n\n        # There should be a total of 9 statements in the database after training\n        self.assertEqual(self.chatbot.storage.count(), 9)\n\n        # The first statement should be in response to another statement\n        first_statement = list(self.chatbot.storage.filter(text=conversation[0]))\n        self.assertIsNone(first_statement[0].in_response_to)\n\n        # The second statement should be in response to the first statement\n        second_statement = list(self.chatbot.storage.filter(text=conversation[1]))\n        self.assertEqual(second_statement[0].in_response_to, conversation[0])\n\n    def test_training_with_unicode_characters(self):\n        \"\"\"\n        Ensure that the training method adds unicode statements\n        to the database.\n        \"\"\"\n        conversation = [\n            u'¶ ∑ ∞ ∫ π ∈ ℝ² ∖ ⩆ ⩇ ⩈ ⩉ ⩊ ⩋ ⪽ ⪾ ⪿ ⫀ ⫁ ⫂ ⋒ ⋓',\n            u'⊂ ⊃ ⊆ ⊇ ⊈ ⊉ ⊊ ⊋ ⊄ ⊅ ⫅ ⫆ ⫋ ⫌ ⫃ ⫄ ⫇ ⫈ ⫉ ⫊ ⟃ ⟄',\n            u'∠ ∡ ⦛ ⦞ ⦟ ⦢ ⦣ ⦤ ⦥ ⦦ ⦧ ⦨ ⦩ ⦪ ⦫ ⦬ ⦭ ⦮ ⦯ ⦓ ⦔ ⦕ ⦖ ⟀',\n            u'∫ ∬ ∭ ∮ ∯ ∰ ∱ ∲ ∳ ⨋ ⨌ ⨍ ⨎ ⨏ ⨐ ⨑ ⨒ ⨓ ⨔ ⨕ ⨖ ⨗ ⨘ ⨙ ⨚ ⨛ ⨜',\n            u'≁ ≂ ≃ ≄ ⋍ ≅ ≆ ≇ ≈ ≉ ≊ ≋ ≌ ⩯ ⩰ ⫏ ⫐ ⫑ ⫒ ⫓ ⫔ ⫕ ⫖',\n            u'¬ ⫬ ⫭ ⊨ ⊭ ∀ ∁ ∃ ∄ ∴ ∵ ⊦ ⊬ ⊧ ⊩ ⊮ ⊫ ⊯ ⊪ ⊰ ⊱ ⫗ ⫘',\n            u'∧ ∨ ⊻ ⊼ ⊽ ⋎ ⋏ ⟑ ⟇ ⩑ ⩒ ⩓ ⩔ ⩕ ⩖ ⩗ ⩘ ⩙ ⩚ ⩛ ⩜ ⩝ ⩞ ⩟ ⩠ ⩢',\n        ]\n\n        self.trainer.train(conversation)\n\n        response = self.chatbot.get_response(conversation[1])\n\n        self.assertEqual(response.text, conversation[2])\n\n    def test_training_with_emoji_characters(self):\n        \"\"\"\n        Ensure that the training method adds statements containing emojis.\n        \"\"\"\n        conversation = [\n            u'Hi, how are you? 😃',\n            u'I am just dandy 👍',\n            u'Superb! 🎆'\n        ]\n\n        self.trainer.train(conversation)\n\n        response = self.chatbot.get_response(conversation[1])\n\n        self.assertEqual(response.text, conversation[2])\n\n    def test_training_with_unicode_bytestring(self):\n        \"\"\"\n        Test training with an 8-bit bytestring.\n        \"\"\"\n        conversation = [\n            'Hi, how are you?',\n            '\\xe4\\xbd\\xa0\\xe5\\xa5\\xbd\\xe5\\x90\\x97',\n            'Superb!'\n        ]\n\n        self.trainer.train(conversation)\n\n        response = self.chatbot.get_response(conversation[1])\n\n        self.assertEqual(response.text, conversation[2])\n\n    def test_similar_sentence_gets_same_response_multiple_times(self):\n        \"\"\"\n        Tests if the bot returns the same response for the same\n        question (which is similar to the one present in the training set)\n        when asked repeatedly.\n        \"\"\"\n        training_data = [\n            'how do you login to gmail?',\n            'Goto gmail.com, enter your login information and hit enter!'\n        ]\n\n        similar_question = 'how do I login to gmail?'\n\n        self.trainer.train(training_data)\n\n        response_to_trained_set = self.chatbot.get_response(\n            text='how do you login to gmail?',\n            conversation='a'\n        )\n        response1 = self.chatbot.get_response(\n            text=similar_question,\n            conversation='b'\n        )\n        response2 = self.chatbot.get_response(\n            text=similar_question,\n            conversation='c'\n        )\n\n        self.assertEqual(response_to_trained_set.text, training_data[1])\n        self.assertEqual(response1.text, training_data[1])\n        self.assertEqual(response2.text, training_data[1])\n\n    def test_consecutive_trainings_same_responses_different_inputs(self):\n        \"\"\"\n        Test consecutive trainings with the same responses to different inputs.\n        \"\"\"\n        self.trainer.train([\"A\", \"B\", \"C\"])\n        self.trainer.train([\"B\", \"C\", \"D\"])\n\n        response1 = self.chatbot.get_response(\"B\")\n        response2 = self.chatbot.get_response(\"C\")\n\n        self.assertEqual(response1.text, \"C\")\n        self.assertEqual(response2.text, \"D\")\n\n\nclass ChatterBotResponseTests(ChatBotTestCase):\n\n    def setUp(self):\n        super().setUp()\n        \"\"\"\n        Set up a database for testing.\n        \"\"\"\n        self.trainer = ListTrainer(\n            self.chatbot,\n            show_training_progress=False\n        )\n\n        data1 = [\n            \"african or european?\",\n            \"Huh? I... I don't know that.\",\n            \"How do you know so much about swallows?\"\n        ]\n\n        data2 = [\n            \"Siri is adorable\",\n            \"Who is Seri?\",\n            \"Siri is my cat\"\n        ]\n\n        data3 = [\n            \"What... is your quest?\",\n            \"To seek the Holy Grail.\",\n            \"What... is your favourite colour?\",\n            \"Blue.\"\n        ]\n\n        self.trainer.train(data1)\n        self.trainer.train(data2)\n        self.trainer.train(data3)\n\n    def test_answer_to_known_input(self):\n        \"\"\"\n        Test that a matching response is returned\n        when an exact match exists.\n        \"\"\"\n        input_text = \"What... is your favourite colour?\"\n        response = self.chatbot.get_response(input_text)\n\n        self.assertIn(\"Blue\", response.text)\n\n    def test_answer_close_to_known_input(self):\n\n        input_text = \"What is your favourite colour?\"\n        response = self.chatbot.get_response(input_text)\n\n        self.assertIn(\"Blue\", response.text)\n\n    def test_match_has_no_response(self):\n        \"\"\"\n        Make sure that the if the last line in a file matches\n        the input text then a index error does not occur.\n        \"\"\"\n        input_text = \"Siri is my cat\"\n        response = self.chatbot.get_response(input_text)\n\n        self.assertGreater(len(response.text), 0)\n\n    def test_empty_input(self):\n        \"\"\"\n        If empty input is provided, anything may be returned.\n        \"\"\"\n        response = self.chatbot.get_response(\"\")\n\n        self.assertTrue(len(response.text) >= 0)\n"
  },
  {
    "path": "tests/training/test_training.py",
    "content": "from tests.base_case import ChatBotTestCase\nfrom chatterbot.trainers import Trainer\nfrom chatterbot.conversation import Statement\n\n\nclass TrainingTests(ChatBotTestCase):\n\n    def setUp(self):\n        super().setUp()\n\n        self.trainer = Trainer(self.chatbot)\n\n    def test_trainer_not_set(self):\n        with self.assertRaises(Trainer.TrainerInitializationException):\n            self.trainer.train()\n\n    def test_generate_export_data(self):\n        self._create_many_with_search_text([\n            Statement(text='Hello, how are you?'),\n            Statement(text='I am good.', in_response_to='Hello, how are you?')\n        ])\n        data = self.trainer._generate_export_data()\n\n        self.assertEqual(\n            [['Hello, how are you?', 'I am good.']], data\n        )\n"
  },
  {
    "path": "tests/training/test_ubuntu_corpus_training.py",
    "content": "from unittest.mock import Mock\nfrom io import BytesIO\nimport tarfile\nimport os\nimport requests\nfrom tests.base_case import ChatBotTestCase\nfrom chatterbot.trainers import UbuntuCorpusTrainer\n\n\nclass UbuntuCorpusTrainerTestCase(ChatBotTestCase):\n    \"\"\"\n    Test the Ubuntu Corpus trainer class.\n    \"\"\"\n\n    def setUp(self):\n        super().setUp()\n        self.trainer = UbuntuCorpusTrainer(\n            self.chatbot,\n            ubuntu_corpus_data_directory='./.ubuntu_test_data/',\n            show_training_progress=False\n        )\n\n        # Fake download url\n        self.data_download_url = 'https://docs.chatterbot.us/ubuntu_dialogs.tgz'\n\n    def tearDown(self):\n        super().tearDown()\n\n        self._remove_data()\n\n    def _get_data(self):\n\n        data1 = (\n            b'2004-11-04T16:49:00.000Z\ttom\tjane\tHello\\n'\n            b'2004-11-04T16:49:00.000Z\ttom\tjane\tIs anyone there?\\n'\n            b'2004-11-04T16:49:00.000Z\tjane\t\tYes\\n'\n            b'\\n'\n        )\n\n        data2 = (\n            b'2004-11-04T16:49:00.000Z\ttom\tjane\tHello\\n'\n            b'2004-11-04T16:49:00.000Z\ttom\t\tIs anyone there?\\n'\n            b'2004-11-04T16:49:00.000Z\tjane\t\tYes\\n'\n            b'\\n'\n        )\n\n        return data1, data2\n\n    def _remove_data(self):\n        \"\"\"\n        Clean up by removing the corpus data directory.\n        \"\"\"\n        import shutil\n\n        if os.path.exists(self.trainer.data_directory):\n            shutil.rmtree(self.trainer.data_directory)\n\n    def _create_test_corpus(self, data):\n        \"\"\"\n        Create a small tar in a similar format to the\n        Ubuntu corpus file in memory for testing.\n        \"\"\"\n        file_path = os.path.join(self.trainer.data_directory, 'ubuntu_dialogs.tgz')\n        os.makedirs(self.trainer.data_directory, exist_ok=True)\n        tar = tarfile.TarFile(file_path, 'a')\n\n        tsv1 = BytesIO(data[0])\n        tsv2 = BytesIO(data[1])\n\n        tarinfo = tarfile.TarInfo('dialogs/3/1.tsv')\n        tarinfo.size = len(data[0])\n        tar.addfile(tarinfo, fileobj=tsv1)\n\n        tarinfo = tarfile.TarInfo('dialogs/3/2.tsv')\n        tarinfo.size = len(data[1])\n        tar.addfile(tarinfo, fileobj=tsv2)\n\n        tsv1.close()\n        tsv2.close()\n        tar.close()\n\n        return file_path\n\n    def _destroy_test_corpus(self):\n        \"\"\"\n        Remove the test corpus file.\n        \"\"\"\n        file_path = os.path.join(self.trainer.data_directory, 'ubuntu_dialogs.tgz')\n\n        if os.path.exists(file_path):\n            os.remove(file_path)\n\n    def _mock_get_response(self, *args, **kwargs):\n        \"\"\"\n        Return a requests.Response object.\n        \"\"\"\n        response = requests.Response()\n        response._content = b'Some response content'\n        response.headers['content-length'] = len(response.content)\n        return response\n\n    def test_download(self):\n        \"\"\"\n        Test the download function for the Ubuntu corpus trainer.\n        \"\"\"\n        requests.get = Mock(side_effect=self._mock_get_response)\n        download_url = 'https://example.com/download.tgz'\n        self.trainer.download(download_url, show_status=False)\n\n        file_name = download_url.split('/')[-1]\n        downloaded_file_path = os.path.join(self.trainer.data_directory, file_name)\n\n        requests.get.assert_called_with(download_url, stream=True)\n        self.assertTrue(os.path.exists(downloaded_file_path))\n\n        # Remove the dummy download_url\n        os.remove(downloaded_file_path)\n\n    def test_download_file_exists(self):\n        \"\"\"\n        Test the case that the corpus file exists.\n        \"\"\"\n        file_path = os.path.join(self.trainer.data_directory, 'download.tgz')\n        os.makedirs(self.trainer.data_directory, exist_ok=True)\n        open(file_path, 'a').close()\n\n        requests.get = Mock(side_effect=self._mock_get_response)\n        download_url = 'https://example.com/download.tgz'\n        self.trainer.download(download_url, show_status=False)\n\n        # Remove the dummy download_url\n        os.remove(file_path)\n\n        self.assertFalse(requests.get.called)\n\n    def test_download_url_not_found(self):\n        \"\"\"\n        Test the case that the url being downloaded does not exist.\n        \"\"\"\n        self.skipTest('This test needs to be created.')\n\n    def test_extract(self):\n        \"\"\"\n        Test the extraction of text from a decompressed Ubuntu Corpus file.\n        \"\"\"\n        file_object_path = self._create_test_corpus(self._get_data())\n        self.trainer.extract(file_object_path)\n\n        self._destroy_test_corpus()\n        corpus_path = os.path.join(self.trainer.data_path, 'dialogs', '3')\n\n        self.assertTrue(os.path.exists(self.trainer.data_path))\n        self.assertTrue(os.path.exists(os.path.join(corpus_path, '1.tsv')))\n        self.assertTrue(os.path.exists(os.path.join(corpus_path, '2.tsv')))\n\n    def test_train(self):\n        \"\"\"\n        Test that the chat bot is trained using data from the Ubuntu Corpus.\n        \"\"\"\n        self._create_test_corpus(self._get_data())\n\n        self.trainer.train(self.data_download_url, limit=50)\n        self._destroy_test_corpus()\n\n        response = self.chatbot.get_response('Is anyone there?')\n        self.assertEqual(response.text, 'Yes')\n\n    def test_train_sets_search_text(self):\n        \"\"\"\n        Test that the chat bot is trained using data from the Ubuntu Corpus.\n        \"\"\"\n        self._create_test_corpus(self._get_data())\n\n        self.trainer.train(self.data_download_url, limit=50)\n        self._destroy_test_corpus()\n\n        results = list(self.chatbot.storage.filter(text='Is anyone there?'))\n\n        self.assertEqual(len(results), 2, msg='Results: {}'.format(results))\n        self.assertEqual(results[0].search_text, 'AUX:anyone PRON:there')\n\n    def test_train_sets_search_in_response_to(self):\n        \"\"\"\n        Test that the chat bot is trained using data from the Ubuntu Corpus.\n        \"\"\"\n        self._create_test_corpus(self._get_data())\n\n        self.trainer.train(self.data_download_url, limit=50)\n        self._destroy_test_corpus()\n\n        results = list(self.chatbot.storage.filter(in_response_to='Is anyone there?'))\n\n        self.assertEqual(len(results), 2)\n        self.assertEqual(results[0].search_in_response_to, 'AUX:anyone PRON:there')\n\n    def test_is_extracted(self):\n        \"\"\"\n        Test that a check can be done for if the corpus has aleady been extracted.\n        \"\"\"\n        file_object_path = self._create_test_corpus(self._get_data())\n        self.trainer.extract(file_object_path)\n\n        extracted = self.trainer.is_extracted(self.trainer.data_path)\n        self._destroy_test_corpus()\n\n        self.assertTrue(extracted)\n\n    def test_is_not_extracted(self):\n        \"\"\"\n        Test that a check can be done for if the corpus has aleady been extracted.\n        \"\"\"\n        self._remove_data()\n        extracted = self.trainer.is_extracted(self.trainer.data_path)\n\n        self.assertFalse(extracted)\n"
  }
]