Repository: gunthercox/ChatterBot Branch: master Commit: 7acf675b204d Files: 236 Total size: 1.3 MB Directory structure: gitextract_if0fu1k3/ ├── .codeclimate.yml ├── .github/ │ ├── FUNDING.yml │ └── workflows/ │ ├── publish-documentation.yml │ └── python-package.yml ├── .gitignore ├── CODE_OF_CONDUCT.md ├── LICENSE ├── README.md ├── SECURITY.md ├── chatterbot/ │ ├── __init__.py │ ├── __main__.py │ ├── adapters.py │ ├── chatterbot.py │ ├── comparisons.py │ ├── components.py │ ├── constants.py │ ├── conversation.py │ ├── corpus.py │ ├── exceptions.py │ ├── ext/ │ │ ├── __init__.py │ │ ├── django_chatterbot/ │ │ │ ├── __init__.py │ │ │ ├── abstract_models.py │ │ │ ├── admin.py │ │ │ ├── apps.py │ │ │ ├── migrations/ │ │ │ │ ├── 0001_initial.py │ │ │ │ ├── 0002_statement_extra_data.py │ │ │ │ ├── 0003_change_occurrence_default.py │ │ │ │ ├── 0004_rename_in_response_to.py │ │ │ │ ├── 0005_statement_created_at.py │ │ │ │ ├── 0006_create_conversation.py │ │ │ │ ├── 0007_response_created_at.py │ │ │ │ ├── 0008_update_conversations.py │ │ │ │ ├── 0009_tags.py │ │ │ │ ├── 0010_statement_text.py │ │ │ │ ├── 0011_blank_extra_data.py │ │ │ │ ├── 0012_statement_created_at.py │ │ │ │ ├── 0013_change_conversations.py │ │ │ │ ├── 0014_remove_statement_extra_data.py │ │ │ │ ├── 0015_statement_persona.py │ │ │ │ ├── 0016_statement_stemmed_text.py │ │ │ │ ├── 0017_tags_unique.py │ │ │ │ ├── 0018_text_max_length.py │ │ │ │ ├── 0019_alter_statement_id_alter_tag_id_and_more.py │ │ │ │ ├── 0020_alter_statement_conversation_and_more.py │ │ │ │ ├── 0021_increase_text_max_length_to_1100.py │ │ │ │ └── __init__.py │ │ │ ├── model_admin.py │ │ │ ├── models.py │ │ │ └── settings.py │ │ └── sqlalchemy_app/ │ │ ├── __init__.py │ │ └── models.py │ ├── filters.py │ ├── languages.py │ ├── logic/ │ │ ├── __init__.py │ │ ├── best_match.py │ │ ├── llm_adapters.py │ │ ├── logic_adapter.py │ │ ├── mathematical_evaluation.py │ │ ├── mcp_tools.py │ │ ├── specific_response.py │ │ ├── time_adapter.py │ │ └── unit_conversion.py │ ├── parsing.py │ ├── preprocessors.py │ ├── response_selection.py │ ├── search.py │ ├── storage/ │ │ ├── __init__.py │ │ ├── django_storage.py │ │ ├── mongodb.py │ │ ├── redis.py │ │ ├── sql_storage.py │ │ └── storage_adapter.py │ ├── tagging.py │ ├── trainers.py │ ├── utils.py │ └── vectorstores.py ├── docs/ │ ├── _ext/ │ │ ├── canonical.py │ │ └── github.py │ ├── _includes/ │ │ └── python_module_structure.txt │ ├── _static/ │ │ ├── mobile.js │ │ ├── silktide-consent-manager.css │ │ ├── silktide-consent-manager.js │ │ └── style.css │ ├── _templates/ │ │ ├── footer.html │ │ ├── layout.html │ │ ├── page.html │ │ └── sidebar_ad.html │ ├── chatterbot.rst │ ├── commands.rst │ ├── comparisons.rst │ ├── conf.py │ ├── contributing.rst │ ├── conversations.rst │ ├── corpus.rst │ ├── development.rst │ ├── django/ │ │ ├── custom-models.rst │ │ ├── index.rst │ │ ├── settings.rst │ │ ├── tutorial/ │ │ │ ├── django-filter-tutorial/ │ │ │ │ └── index.rst │ │ │ ├── django-rest-framework-tutorial/ │ │ │ │ └── index.rst │ │ │ ├── django-tutorial/ │ │ │ │ ├── index.rst │ │ │ │ └── part-2.rst │ │ │ ├── index.rst │ │ │ └── writing-tests.rst │ │ ├── views.rst │ │ └── wsgi.rst │ ├── encoding.rst │ ├── examples.rst │ ├── faq.rst │ ├── filters.rst │ ├── glossary.rst │ ├── index.rst │ ├── large-language-models.rst │ ├── logic/ │ │ ├── create-a-logic-adapter.rst │ │ ├── index.rst │ │ └── response-selection.rst │ ├── packaging.rst │ ├── preprocessors.rst │ ├── quickstart.rst │ ├── releases.rst │ ├── robots.txt │ ├── security.rst │ ├── setup.rst │ ├── statements.txt │ ├── storage/ │ │ ├── create-a-storage-adapter.rst │ │ ├── index.rst │ │ ├── mongodb.rst │ │ ├── redis.rst │ │ ├── sql.rst │ │ └── text-search.rst │ ├── testing.rst │ ├── training.rst │ ├── tutorial.rst │ ├── upgrading.rst │ └── utils.rst ├── examples/ │ ├── __init__.py │ ├── basic_example.py │ ├── convert_units.py │ ├── default_response_example.py │ ├── django_example/ │ │ ├── README.rst │ │ ├── django_example/ │ │ │ ├── __init__.py │ │ │ ├── asgi.py │ │ │ ├── management/ │ │ │ │ ├── __init__.py │ │ │ │ └── commands/ │ │ │ │ ├── __init__.py │ │ │ │ └── train.py │ │ │ ├── settings.py │ │ │ ├── static/ │ │ │ │ ├── css/ │ │ │ │ │ ├── bootstrap.css │ │ │ │ │ └── custom.css │ │ │ │ └── js/ │ │ │ │ ├── bootstrap.js │ │ │ │ ├── jquery.js │ │ │ │ └── js.cookie.js │ │ │ ├── templates/ │ │ │ │ ├── app.html │ │ │ │ └── nav.html │ │ │ ├── tests/ │ │ │ │ ├── __init__.py │ │ │ │ ├── test_api.py │ │ │ │ └── test_example.py │ │ │ ├── urls.py │ │ │ ├── views.py │ │ │ └── wsgi.py │ │ ├── manage.py │ │ └── requirements.txt │ ├── export_example.py │ ├── learning_feedback_example.py │ ├── math_and_time.py │ ├── memory_sql_example.py │ ├── ollama_example.py │ ├── openai_example.py │ ├── specific_response_example.py │ ├── tagged_dataset_example.py │ ├── terminal_example.py │ ├── terminal_mongo_example.py │ ├── tkinter_gui.py │ ├── training_example_chatterbot_corpus.py │ ├── training_example_list_data.py │ └── training_example_ubuntu_corpus.py ├── graphics/ │ ├── README.md │ ├── ad.xcf │ └── chatterbot.xcf ├── pyproject.toml ├── setup.cfg └── tests/ ├── __init__.py ├── base_case.py ├── django_integration/ │ ├── __init__.py │ ├── base_case.py │ ├── test_chatbot.py │ ├── test_chatterbot_corpus_training.py │ ├── test_chatterbot_settings.py │ ├── test_custom_models.py │ ├── test_django_adapter.py │ ├── test_logic_adapter_integration.py │ ├── test_secondary_database.py │ ├── test_settings.py │ └── test_statement_integration.py ├── logic/ │ ├── __init__.py │ ├── test_best_match.py │ ├── test_data_cache.py │ ├── test_logic_adapter.py │ ├── test_mathematical_evaluation.py │ ├── test_specific_response.py │ ├── test_time.py │ └── test_unit_conversion.py ├── storage/ │ ├── __init__.py │ ├── test_mongo_adapter.py │ ├── test_redis_adapter.py │ ├── test_sql_adapter.py │ └── test_storage_adapter.py ├── test_adapter_validation.py ├── test_benchmarks.py ├── test_chatbot.py ├── test_cli.py ├── test_comparisons.py ├── test_conversations.py ├── test_corpus.py ├── test_examples.py ├── test_filters.py ├── test_initialization.py ├── test_languages.py ├── test_parsing.py ├── test_preprocessors.py ├── test_response_selection.py ├── test_search.py ├── test_tagging.py ├── test_turing.py ├── test_utils.py └── training/ ├── __init__.py ├── test_chatterbot_corpus_training.py ├── test_csv_file_training.py ├── test_data/ │ ├── csv_corpus/ │ │ ├── 1.csv │ │ └── 2.csv │ ├── get_search.json │ └── json_corpus/ │ ├── 1.json │ └── 2.json ├── test_json_file_training.py ├── test_list_training.py ├── test_training.py └── test_ubuntu_corpus_training.py ================================================ FILE CONTENTS ================================================ ================================================ FILE: .codeclimate.yml ================================================ engines: pep8: enabled: true ratings: paths: - "**.py" exclude_paths: - tests/* - examples/* - chatterbot/ext/django_chatterbot/migrations/* ================================================ FILE: .github/FUNDING.yml ================================================ github: gunthercox ================================================ FILE: .github/workflows/publish-documentation.yml ================================================ name: Deploy Sphinx documentation to Pages on: push: branches: [master] # branch to trigger deployment jobs: pages: runs-on: ubuntu-latest environment: name: github-pages url: ${{ steps.deployment.outputs.page_url }} permissions: pages: write id-token: write steps: - id: deployment uses: sphinx-notes/pages@v3 with: pyproject_extras: 'test' sphinx_build_options: '-b dirhtml' ================================================ FILE: .github/workflows/python-package.yml ================================================ # This workflow will install Python dependencies, run tests and lint with a variety of Python versions # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python name: Python package permissions: contents: read pull-requests: write checks: write on: push: branches: [ "master" ] pull_request: branches: [ "*" ] env: CHATTERBOT_SHOW_TRAINING_PROGRESS: '0' jobs: build: runs-on: ubuntu-latest strategy: fail-fast: false matrix: python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"] services: redis: image: redis/redis-stack-server:latest ports: - 6379:6379 steps: - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} cache: 'pip' - name: Install dependencies run: | python -m pip install --upgrade pip pip install .[test,dev,redis,mongodb] python -m spacy download en_core_web_sm python -m spacy download de_core_news_sm - name: Lint with flake8 run: | # stop the build if there are Python syntax errors or undefined names flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics - name: Start MongoDB uses: supercharge/mongodb-github-action@1.11.0 with: mongodb-version: '8.0' - name: Run tests run: | python -Wonce -m unittest discover -s tests -v - name: Run tests for Django example app run: | python -Wonce examples/django_example/manage.py test examples/django_example/ # -------------------------------------------------------------- # TODO: Fix & re-enable later # https://github.com/marketplace/actions/coveralls-github-action # - name: Coveralls GitHub Action # uses: coverallsapp/github-action@v2.3.4 # - name: Generate code coverage # uses: paambaati/codeclimate-action@v9.0.0 # env: # CC_TEST_REPORTER_ID: 3ec30a156224df0f59620967241d9659086e918fd824f4f69b8ce7b55b5a590f # with: # coverageCommand: coverage # debug: true ================================================ FILE: .gitignore ================================================ bin build html dist venv .env .out .coverage .python-version *.pyc *.swp *.egg-info *.egg/* *.eggs/* *.doctrees # Database files .database *.sqlite3 *.sqlite3-* # IDE files .vscode ================================================ FILE: CODE_OF_CONDUCT.md ================================================ # Contributor Covenant Code of Conduct ## Our Pledge We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation. We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community. ## Our Standards Examples of behavior that contributes to a positive environment for our community include: * Demonstrating empathy and kindness toward other people * Being respectful of differing opinions, viewpoints, and experiences * Giving and gracefully accepting constructive feedback * Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience * Focusing on what is best not just for us as individuals, but for the overall community Examples of unacceptable behavior include: * The use of sexualized language or imagery, and sexual attention or advances of any kind * Trolling, insulting or derogatory comments, and personal or political attacks * Public or private harassment * Publishing others' private information, such as a physical or email address, without their explicit permission * Other conduct which could reasonably be considered inappropriate in a professional setting ## Enforcement Responsibilities Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful. Community leaders have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, and will communicate reasons for moderation decisions when appropriate. ## Scope This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public spaces. Examples of representing our community include using an official e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at community@chatterbot.us. All complaints will be reviewed and investigated promptly and fairly. All community leaders are obligated to respect the privacy and security of the reporter of any incident. ## Enforcement Guidelines Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct: ### 1. Correction **Community Impact**: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community. **Consequence**: A private, written warning from community leaders, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested. ### 2. Warning **Community Impact**: A violation through a single incident or series of actions. **Consequence**: A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding interactions in community spaces as well as external channels like social media. Violating these terms may lead to a temporary or permanent ban. ### 3. Temporary Ban **Community Impact**: A serious violation of community standards, including sustained inappropriate behavior. **Consequence**: A temporary ban from any sort of interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban. ### 4. Permanent Ban **Community Impact**: Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals. **Consequence**: A permanent ban from any sort of public interaction within the community. ## Attribution This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.0, available at https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. Community Impact Guidelines were inspired by [Mozilla's code of conduct enforcement ladder](https://github.com/mozilla/diversity). [homepage]: https://www.contributor-covenant.org For answers to common questions about this code of conduct, see the FAQ at https://www.contributor-covenant.org/faq. Translations are available at https://www.contributor-covenant.org/translations. ================================================ FILE: LICENSE ================================================ Copyright (c) 2016 - 2025, Gunther Cox All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * 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. * 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. THIS 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. ================================================ FILE: README.md ================================================ ![ChatterBot: Machine learning in Python](https://i.imgur.com/b3SCmGT.png) # ChatterBot ChatterBot is a machine-learning based conversational dialog engine built in Python which makes it possible to generate responses based on collections of known conversations. The language independent design of ChatterBot allows it to be trained to speak any language. [![Package Version](https://img.shields.io/pypi/v/chatterbot.svg)](https://pypi.python.org/pypi/chatterbot/) [![Python 3.12](https://img.shields.io/badge/python-3.12-blue.svg)](https://www.python.org/downloads/release/python-360/) [![Coverage Status](https://img.shields.io/coveralls/gunthercox/ChatterBot.svg)](https://coveralls.io/r/gunthercox/ChatterBot) [![Follow on Bluesky](https://img.shields.io/badge/🦋%20Bluesky-1185fe)](https://bsky.app/profile/chatterbot.us) [![Join the chat at https://gitter.im/chatterbot/Lobby](https://badges.gitter.im/chatterbot/Lobby.svg)](https://gitter.im/chatterbot/Lobby?utm_source=badge&utm_medium=badge&utm_content=badge) An example of typical input would be something like this: > **user:** Good morning! How are you doing? > **bot:** I am doing very well, thank you for asking. > **user:** You're welcome. > **bot:** Do you like hats? ## How it works An untrained instance of ChatterBot starts off with no knowledge of how to communicate. Each time a user enters a statement, the library saves the text that they entered and the text that the statement was in response to. As ChatterBot receives more input the number of responses that it can reply 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. # [Documentation](https://docs.chatterbot.us) View the [documentation](https://docs.chatterbot.us) for ChatterBot. ## Installation This package can be installed from [PyPi](https://pypi.python.org/pypi/ChatterBot) by running: ```bash pip install chatterbot ``` ## Basic Usage ```python from chatterbot import ChatBot from chatterbot.trainers import ChatterBotCorpusTrainer chatbot = ChatBot('Ron Obvious') # Create a new trainer for the chatbot trainer = ChatterBotCorpusTrainer(chatbot) # Train the chatbot based on the english corpus trainer.train("chatterbot.corpus.english") # Get a response to an input statement chatbot.get_response("Hello, how are you today?") ``` # Training data ChatterBot comes with a data utility module that can be used to train chat bots. At the moment there is training data for over a dozen languages in this module. Contributions of additional training data or training data in other languages would be greatly appreciated. Take a look at the data files in the [chatterbot-corpus](https://github.com/gunthercox/chatterbot-corpus) package if you are interested in contributing. ```python from chatterbot.trainers import ChatterBotCorpusTrainer # Create a new trainer for the chatbot trainer = ChatterBotCorpusTrainer(chatbot) # Train based on the english corpus trainer.train("chatterbot.corpus.english") # Train based on english greetings corpus trainer.train("chatterbot.corpus.english.greetings") # Train based on the english conversations corpus trainer.train("chatterbot.corpus.english.conversations") ``` **Corpus contributions are welcome! Please make a pull request.** # Examples For examples, see the [examples](https://docs.chatterbot.us/examples/) section of the documentation. # History See release notes for changes https://github.com/gunthercox/ChatterBot/releases # Contributing Contributions are welcomed, to help ensure a smooth process please start with the contributing guidelines in our documentation: https://docs.chatterbot.us/contributing/ # Sponsors ChatterBot is sponsored by:

# License ChatterBot is licensed under the [BSD 3-clause license](https://opensource.org/licenses/BSD-3-Clause). ================================================ FILE: SECURITY.md ================================================ # Security Policy ## Supported Versions Actively supported versions of ChatterBot can be determined using the following table. | Version | Supported | | ------- | ------------------ | | 1.2.x | :white_check_mark: | | < 1.2 | :x: | ## Reporting a Vulnerability ChatterBot uses GitHub's private security vulnerability reporting to accept reports about potential security vulnerabilities. https://github.com/gunthercox/ChatterBot/security/advisories To begin the process select the "Report a vulnerability" button on the security advisories page. Once the report has been investigated an response plan will be issued based on the level of severity. A response can generally be expected within 24 hours of report submission. ================================================ FILE: chatterbot/__init__.py ================================================ """ ChatterBot is a machine learning, conversational dialog engine. """ from .chatterbot import ChatBot __version__ = '1.2.12' __all__ = ( 'ChatBot', ) ================================================ FILE: chatterbot/__main__.py ================================================ """ Example usage for ChatterBot command line arguments: python -m chatterbot --help """ import sys def get_chatterbot_version(): """ Return the version of the current package. """ from chatterbot import __version__ return __version__ if __name__ == '__main__': if '--version' in sys.argv: print(get_chatterbot_version()) elif '--help' in sys.argv: print('usage: chatterbot [--version, --help]') print(' --version: Print the version of ChatterBot') print(' --help: Print this help message') print() print('Documentation at https://docs.chatterbot.us') ================================================ FILE: chatterbot/adapters.py ================================================ class Adapter(object): """ A superclass for all adapter classes. :param chatbot: A ChatBot instance. """ def __init__(self, chatbot, **kwargs): self.chatbot = chatbot class AdapterMethodNotImplementedError(NotImplementedError): """ An exception to be raised when an adapter method has not been implemented. Typically this indicates that the developer is expected to implement the method in a subclass. """ def __init__(self, message='This method must be overridden in a subclass method.'): """ Set the message for the exception. """ super().__init__(message) class InvalidAdapterTypeException(Exception): """ An exception to be raised when an adapter of an unexpected class type is received. """ pass ================================================ FILE: chatterbot/chatterbot.py ================================================ import logging import uuid from typing import Union from chatterbot.storage import StorageAdapter from chatterbot.logic import LogicAdapter from chatterbot.search import TextSearch, IndexedTextSearch, SemanticVectorSearch from chatterbot.tagging import PosLemmaTagger from chatterbot.conversation import Statement from chatterbot import languages from chatterbot import utils import spacy class ChatBot(object): """ A conversational dialog chat bot. :param name: A name is the only required parameter for the ChatBot class. :type name: str :keyword storage_adapter: The dot-notated import path to a storage adapter class. Defaults to ``"chatterbot.storage.SQLStorageAdapter"``. :type storage_adapter: str :param logic_adapters: A list of dot-notated import paths to each logic adapter the bot uses. Defaults to ``["chatterbot.logic.BestMatch"]``. :type logic_adapters: list :param tagger: The tagger to use for the chat bot. Defaults to :class:`~chatterbot.tagging.PosLemmaTagger` :type tagger: object :param tagger_language: The language to use for the tagger. Defaults to :class:`~chatterbot.languages.ENG`. :type tagger_language: object :param preprocessors: A list of preprocessor functions to use for the chat bot. :type preprocessors: list :param read_only: If True, the chat bot will not save any input it receives, defaults to False. :type read_only: bool :param logger: A ``Logger`` object. :type logger: logging.Logger :param model: A definition used to load a large language model. Defaults to ``None``. (Added in version 1.2.7) :type model: dict :param stream: Return output as a streaming responses when a ``model`` is defined. (Added in version 1.2.7) """ def __init__(self, name, stream=False, **kwargs): self.name = name self.stream = stream # Generate a default conversation ID for this ChatBot instance. # This is used as a fallback when callers don't provide an explicit # conversation ID, ensuring that conversation history is tracked # within a session. Conversation IDs are necessary for cases such as # the LLM-based logic adapters which require it to retrieve previous # messages. self.default_conversation = uuid.uuid4().hex self.logger = kwargs.get('logger', logging.getLogger(__name__)) storage_adapter = kwargs.get('storage_adapter', 'chatterbot.storage.SQLStorageAdapter') logic_adapters = kwargs.get('logic_adapters', [ 'chatterbot.logic.BestMatch' ]) # Check that each adapter is a valid subclass of it's respective parent utils.validate_adapter_class(storage_adapter, StorageAdapter) # Logic adapters used by the chat bot self.logic_adapters = [] self.storage = utils.initialize_class(storage_adapter, **kwargs) tagger_language = kwargs.get('tagger_language', languages.ENG) # Check if storage adapter has a preferred tagger PreferredTagger = self.storage.get_preferred_tagger() if PreferredTagger is not None: # Storage adapter specifies its own tagger self.tagger = PreferredTagger(language=tagger_language) else: # Use default or user-specified tagger try: Tagger = kwargs.get('tagger', PosLemmaTagger) # Allow instances to be provided for performance optimization # (Example: a pre-loaded model in a tagger when unit testing) if not isinstance(Tagger, type): self.tagger = Tagger else: self.tagger = Tagger(language=tagger_language) except IOError as io_error: # Return a more helpful error message if possible if "Can't find model" in str(io_error): model_name = utils.get_model_for_language(tagger_language) if hasattr(tagger_language, 'ENGLISH_NAME'): language_name = tagger_language.ENGLISH_NAME else: language_name = tagger_language raise self.ChatBotException( 'Setup error:\n' f'The Spacy model for "{language_name}" language is missing.\n' 'Please install the model using the command:\n\n' f'python -m spacy download {model_name}\n\n' 'See https://spacy.io/usage/models for more information about available models.' ) from io_error else: raise io_error # Initialize search algorithms primary_search_algorithm = IndexedTextSearch(self, **kwargs) text_search_algorithm = TextSearch(self, **kwargs) semantic_vector_search_algorithm = SemanticVectorSearch(self, **kwargs) self.search_algorithms = { primary_search_algorithm.name: primary_search_algorithm, text_search_algorithm.name: text_search_algorithm, semantic_vector_search_algorithm.name: semantic_vector_search_algorithm } # Check if storage adapter has a preferred search algorithm preferred_search_algorithm = self.storage.get_preferred_search_algorithm() if preferred_search_algorithm and preferred_search_algorithm in self.search_algorithms: # Set as default for logic adapters that don't specify their own search algorithm # This ensures BestMatch and other adapters use the optimal search method self.logger.info(f'Storage adapter prefers search algorithm: {preferred_search_algorithm}') kwargs.setdefault('search_algorithm_name', preferred_search_algorithm) for adapter in logic_adapters: utils.validate_adapter_class(adapter, LogicAdapter) logic_adapter = utils.initialize_class(adapter, self, **kwargs) self.logic_adapters.append(logic_adapter) preprocessors = kwargs.get( 'preprocessors', [ 'chatterbot.preprocessors.clean_whitespace' ] ) self.preprocessors = [] for preprocessor in preprocessors: self.preprocessors.append(utils.import_module(preprocessor)) # NOTE: 'xx' is the language code for a multi-language model self.nlp = spacy.blank(self.tagger.language.ISO_639_1) # Allow the bot to save input it receives so that it can learn self.read_only = kwargs.get('read_only', False) def get_response(self, statement: Union[Statement, str, dict] = None, **kwargs) -> Statement: """ Return the bot's response based on the input. :param statement: An statement object or string. :returns: A response to the input. :param additional_response_selection_parameters: Parameters to pass to the chat bot's logic adapters to control response selection. :type additional_response_selection_parameters: dict :param persist_values_to_response: Values that should be saved to the response that the chat bot generates. :type persist_values_to_response: dict """ Statement = self.storage.get_object('statement') additional_response_selection_parameters = kwargs.pop('additional_response_selection_parameters', {}) persist_values_to_response = kwargs.pop('persist_values_to_response', {}) if isinstance(statement, str): kwargs['text'] = statement if isinstance(statement, dict): kwargs.update(statement) if statement is None and 'text' not in kwargs: raise self.ChatBotException( 'Either a statement object or a "text" keyword ' 'argument is required. Neither was provided.' ) if hasattr(statement, 'serialize'): kwargs.update(**statement.serialize()) tags = kwargs.pop('tags', []) text = kwargs.pop('text') input_statement = Statement(text=text, **kwargs) input_statement.add_tags(*tags) # If no conversation ID was provided, use the default session ID # so that conversation history is tracked across calls. Callers # can override this by passing an explicit conversation kwarg or # setting it on the Statement object. if not input_statement.conversation: input_statement.conversation = self.default_conversation # Preprocess the input statement for preprocessor in self.preprocessors: input_statement = preprocessor(input_statement) # Mark the statement as being a response to the previous if input_statement.in_response_to is None: previous_statement = self.get_latest_response(input_statement.conversation) if previous_statement: input_statement.in_response_to = previous_statement.text # Make sure the input statement has its search text saved if not self.tagger.needs_text_indexing(): # Tagger doesn't transform text, use it directly if not input_statement.search_text: input_statement.search_text = input_statement.text if not input_statement.search_in_response_to and input_statement.in_response_to: input_statement.search_in_response_to = input_statement.in_response_to else: # Use tagger for text indexing or transformations if not input_statement.search_text: _search_text = self.tagger.get_text_index_string(input_statement.text) input_statement.search_text = _search_text if not input_statement.search_in_response_to and input_statement.in_response_to: input_statement.search_in_response_to = self.tagger.get_text_index_string( input_statement.in_response_to ) response = self.generate_response( input_statement, additional_response_selection_parameters ) # If streaming is enabled return the response immediately if self.stream: return response # Update any response data that needs to be changed if persist_values_to_response: for response_key in persist_values_to_response: response_value = persist_values_to_response[response_key] if response_key == 'tags': input_statement.add_tags(*response_value) response.add_tags(*response_value) else: setattr(input_statement, response_key, response_value) setattr(response, response_key, response_value) if not self.read_only: # Save the input statement self.storage.create(**input_statement.serialize()) # Save the response generated for the input self.learn_response(response, previous_statement=input_statement) return response def generate_response(self, input_statement, additional_response_selection_parameters=None): """ Return a response based on a given input statement. :param input_statement: The input statement to be processed. """ Statement = self.storage.get_object('statement') results = [] result = None max_confidence = -1 for adapter in self.logic_adapters: if adapter.can_process(input_statement): output = adapter.process(input_statement, additional_response_selection_parameters) results.append(output) self.logger.info( '{} selected "{}" as a response with a confidence of {}'.format( adapter.class_name, output.text, output.confidence ) ) if output.confidence > max_confidence: result = output max_confidence = output.confidence else: self.logger.info( 'Not processing the statement using {}'.format(adapter.class_name) ) class ResultOption: def __init__(self, statement, count=1): self.statement = statement self.count = count # If multiple adapters agree on the same statement, # then that statement is more likely to be the correct response if len(results) >= 3: result_options = {} for result_option in results: result_string = result_option.text + ':' + (result_option.in_response_to or '') if result_string in result_options: result_options[result_string].count += 1 if result_options[result_string].statement.confidence < result_option.confidence: result_options[result_string].statement = result_option else: result_options[result_string] = ResultOption( result_option ) most_common = list(result_options.values())[0] for result_option in result_options.values(): if result_option.count > most_common.count: most_common = result_option self.logger.info('Selecting "{}" as the most common response'.format(most_common.statement.text)) if most_common.count > 1: result = most_common.statement response = Statement( text=result.text, in_response_to=input_statement.text, conversation=input_statement.conversation, persona='bot:' + self.name ) response.add_tags(*result.get_tags()) response.confidence = result.confidence return response def learn_response(self, statement, previous_statement=None): """ Learn that the statement provided is a valid response. """ if not previous_statement: previous_statement = statement.in_response_to if not previous_statement: previous_statement = self.get_latest_response(statement.conversation) if previous_statement: previous_statement = previous_statement.text previous_statement_text = previous_statement if not isinstance(previous_statement, (str, type(None), )): statement.in_response_to = previous_statement.text elif isinstance(previous_statement, str): statement.in_response_to = previous_statement self.logger.info('Adding "{}" as a response to "{}"'.format( statement.text, previous_statement_text )) if not statement.persona: statement.persona = 'bot:' + self.name # Save the response statement return self.storage.create(**statement.serialize()) def get_latest_response(self, conversation: str): """ Returns the latest response in a conversation if it exists. Returns None if a matching conversation cannot be found. """ conversation_statements = list(self.storage.filter( conversation=conversation, order_by=['id'] )) # Get the most recent statement in the conversation if one exists latest_statement = conversation_statements[-1] if len(conversation_statements) else None return latest_statement class ChatBotException(Exception): pass ================================================ FILE: chatterbot/comparisons.py ================================================ """ This module contains various text-comparison algorithms designed to compare one statement to another. """ from chatterbot.utils import get_model_for_language from difflib import SequenceMatcher import spacy class Comparator: """ Base class establishing the interface that all comparators should implement. """ def __init__(self, language): self.language = language def __call__(self, statement_a, statement_b): return self.compare(statement_a, statement_b) def compare_text(self, text_a: str, text_b: str) -> float: """ Implemented in subclasses: compare text_a to text_b. :return: The percent of similarity between the statements based on the implemented algorithm. """ return 0 def compare(self, statement_a, statement_b) -> float: """ :return: The percent of similarity between the statements based on the implemented algorithm. """ return self.compare_text(statement_a.text, statement_b.text) class LevenshteinDistance(Comparator): """ Compare two statements based on the Levenshtein distance of each statement's text. For example, there is a 65% similarity between the statements "where is the post office?" and "looking for the post office" based on the Levenshtein distance algorithm. """ def compare_text(self, text_a: str, text_b: str) -> float: """ Compare the two pieces of text. :return: The percent of similarity between the text of the statements. """ # Return 0 if either statement has a None text value if text_a is None or text_b is None: return 0 # Get the lowercase version of both strings statement_a_text = str(text_a.lower()) statement_b_text = str(text_b.lower()) similarity = SequenceMatcher( None, statement_a_text, statement_b_text ) # Calculate a decimal percent of the similarity percent = round(similarity.ratio(), 2) return percent class SpacySimilarity(Comparator): """ Calculate the similarity of two statements using Spacy models. NOTE: You will also need to download a ``spacy`` model to use for tagging. Internally these are used to determine parts of speech for words. The easiest way to do this is to use the ``spacy download`` command directly: .. code-block:: python python -m spacy download en_core_web_sm python -m spacy download de_core_news_sm Alternatively, the ``spacy`` models can be installed as Python packages. The following lines could be included in a ``requirements.txt`` or ``pyproject.yml`` file if you needed to pin specific versions: .. code-block:: text 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 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 """ def __init__(self, language): super().__init__(language) model = get_model_for_language(language) # Disable the Named Entity Recognition (NER) component because it is not necessary self.nlp = spacy.load(model, exclude=['ner']) def compare_text(self, text_a: str, text_b: str) -> float: """ Compare the similarity of two strings. :return: The percent of similarity between the closest synset distance. """ # Return 0 if either statement has a None text value if text_a is None or text_b is None: return 0 document_a = self.nlp(text_a) document_b = self.nlp(text_b) return document_a.similarity(document_b) class JaccardSimilarity(Comparator): """ Calculates the similarity of two statements based on the Jaccard index. The Jaccard index is composed of a numerator and denominator. In the numerator, we count the number of items that are shared between the sets. In the denominator, we count the total number of items across both sets. Let's say we define sentences to be equivalent if 50% or more of their tokens are equivalent. Here are two sample sentences: The young cat is hungry. The cat is very hungry. When we parse these sentences to remove stopwords, we end up with the following two sets: {young, cat, hungry} {cat, very, hungry} In our example above, our intersection is {cat, hungry}, which has count of two. The union of the sets is {young, cat, very, hungry}, which has a count of four. Therefore, our `Jaccard similarity index`_ is two divided by four, or 50%. Given our similarity threshold above, we would consider this to be a match. .. _`Jaccard similarity index`: https://en.wikipedia.org/wiki/Jaccard_index """ def __init__(self, language): super().__init__(language) model = get_model_for_language(language) # Disable the Named Entity Recognition (NER) component because it is not necessary self.nlp = spacy.load(model, exclude=['ner']) def compare_text(self, text_a: str, text_b: str) -> float: """ Return the calculated similarity of two statements based on the Jaccard index. """ # Return 0 if either statement has a None text value if text_a is None or text_b is None: return 0 # Make both strings lowercase document_a = self.nlp(text_a.lower()) document_b = self.nlp(text_b.lower()) statement_a_lemmas = frozenset([ token.lemma_ for token in document_a if not token.is_stop ]) statement_b_lemmas = frozenset([ token.lemma_ for token in document_b if not token.is_stop ]) # Calculate Jaccard similarity numerator = len(statement_a_lemmas.intersection(statement_b_lemmas)) denominator = float(len(statement_a_lemmas.union(statement_b_lemmas))) ratio = numerator / denominator return ratio ================================================ FILE: chatterbot/components.py ================================================ """ Custom components for Spacy processing pipelines. https://spacy.io/usage/processing-pipelines#custom-components """ import string from spacy.language import Language from spacy.tokens import Doc punctuation_table = str.maketrans(dict.fromkeys(string.punctuation)) @Language.component('chatterbot_bigram_indexer') def chatterbot_bigram_indexer(document): """ Generate the text string for a bigram-based search index. """ if not Doc.has_extension('search_index'): Doc.set_extension('search_index', default='') tokens = [ token for token in document if not (token.is_punct or token.is_stop) ] # Fall back to including stop words if needed if not tokens or len(tokens) == 1: tokens = [ token for token in document if not (token.is_punct) ] # Pairs consist of the part-of-speech of the first token and the # lemma of the second token in the bigram. This provides a good # balance of generalization and specificity for matching. bigram_pairs = [ f"{tokens[i - 1].pos_}:{tokens[i].lemma_.lower()}" for i in range(1, len(tokens)) ] if not bigram_pairs: text_without_punctuation = document.text.translate( punctuation_table ) if len(text_without_punctuation) >= 1: text = text_without_punctuation.lower() else: text = document.text.lower() bigram_pairs = [text] # Assign a custom attribute at the Doc level document._.search_index = ' '.join(bigram_pairs) return document @Language.component('chatterbot_lowercase_indexer') def chatterbot_lowercase_indexer(document): """ Generate the a lowercase text string for search index. """ if not Doc.has_extension('search_index'): Doc.set_extension('search_index', default='') # Assign a custom attribute at the Doc level document._.search_index = document.text.lower() return document ================================================ FILE: chatterbot/constants.py ================================================ """ ChatterBot constants """ from chatterbot import languages ''' The maximum length of characters that the text of a statement can contain. The number 1100 is used to support longer conversational statements while remaining within VARCHAR limits for most databases. This value should be enforced on a per-model basis by the data model for each storage adapter. ''' STATEMENT_TEXT_MAX_LENGTH = 1100 ''' The maximum length of characters that the text label of a conversation can contain. The number 32 was chosen because that is the length of the string representation of a UUID4 with no hyphens. ''' CONVERSATION_LABEL_MAX_LENGTH = 32 ''' The maximum length of text that can be stored in the persona field of the statement model. ''' PERSONA_MAX_LENGTH = 50 # The maximum length of characters that the name of a tag can contain TAG_NAME_MAX_LENGTH = 50 # See other model options: https://spacy.io/models/ DEFAULT_LANGUAGE_TO_SPACY_MODEL_MAP = { languages.CAT: 'ca_core_news_sm', languages.CHI: 'zh_core_web_sm', languages.HRV: 'hr_core_news_sm', languages.DAN: 'da_core_news_sm', languages.DUT: 'nl_core_news_sm', languages.ENG: 'en_core_web_sm', languages.FIN: 'fi_core_news_sm', languages.FRE: 'fr_core_news_sm', languages.GER: 'de_core_news_sm', languages.GRE: 'el_core_news_sm', languages.ITA: 'it_core_news_sm', languages.JPN: 'ja_core_news_sm', languages.KOR: 'ko_core_news_sm', languages.LIT: 'lt_core_news_sm', languages.MAC: 'mk_core_news_sm', languages.NOR: 'nb_core_news_sm', languages.POL: 'pl_core_news_sm', languages.POR: 'pt_core_news_sm', languages.RUM: 'ro_core_news_sm', languages.RUS: 'ru_core_news_sm', languages.SLO: 'sl_core_news_sm', languages.SPA: 'es_core_news_sm', languages.SWE: 'sv_core_news_sm', languages.UKR: 'uk_core_news_sm', } DEFAULT_DJANGO_APP_NAME = 'django_chatterbot' ================================================ FILE: chatterbot/conversation.py ================================================ from datetime import datetime, timezone from dateutil import parser as date_parser class StatementMixin(object): """ This class has shared methods used to normalize different statement models. """ statement_field_names = [ 'id', 'text', 'search_text', 'conversation', 'persona', 'tags', 'in_response_to', 'search_in_response_to', 'created_at', ] extra_statement_field_names = [] def get_statement_field_names(self) -> list[str]: """ Return the list of field names for the statement. """ return self.statement_field_names + self.extra_statement_field_names def get_tags(self) -> list[str]: """ Return the list of tags for this statement. """ return self.tags def add_tags(self, *tags): """ Add a list of strings to the statement as tags. """ self.tags.extend(tags) def serialize(self) -> dict: """ :returns: A dictionary representation of the statement object. """ data = {} for field_name in self.get_statement_field_names(): format_method = getattr(self, 'get_{}'.format( field_name ), None) if format_method: data[field_name] = format_method() else: data[field_name] = getattr(self, field_name) return data class Statement(StatementMixin): """ A statement represents a single spoken entity, sentence or phrase that someone can say. """ __slots__ = ( 'id', 'text', 'search_text', 'conversation', 'persona', 'tags', 'in_response_to', 'search_in_response_to', 'created_at', 'confidence', 'storage', ) def __init__(self, text: str, in_response_to=None, **kwargs): self.id = kwargs.get('id') self.text = str(text) self.search_text = kwargs.get('search_text', '') self.conversation = kwargs.get('conversation', '') self.persona = kwargs.get('persona', '') self.tags = kwargs.pop('tags', []) self.in_response_to = in_response_to self.search_in_response_to = kwargs.get('search_in_response_to', '') self.created_at = kwargs.get('created_at', datetime.now()) if not isinstance(self.created_at, datetime): self.created_at = date_parser.parse(self.created_at) # Set timezone to UTC if no timezone was provided if not self.created_at.tzinfo: self.created_at = self.created_at.replace(tzinfo=timezone.utc) # This is the confidence with which the chat bot believes # this is an accurate response. This value is set when the # statement is returned by the chat bot. self.confidence = kwargs.get('confidence', 0) self.storage = None def __str__(self): return self.text def __repr__(self): return '' % (self.text) def save(self): """ Save the statement in the database. """ self.storage.update(self) ================================================ FILE: chatterbot/corpus.py ================================================ import os import io import glob from pathlib import Path from chatterbot.exceptions import OptionalDependencyImportError try: from chatterbot_corpus.corpus import DATA_DIRECTORY except (ImportError, ModuleNotFoundError): # Default to the home directory of the current user DATA_DIRECTORY = os.path.join( Path.home(), 'chatterbot_corpus', 'data' ) CORPUS_EXTENSION = 'yml' def get_file_path(dotted_path, extension='json') -> str: """ Reads a dotted file path and returns the file path. """ # If the operating system's file path seperator character is in the string if os.sep in dotted_path or '/' in dotted_path: # Assume the path is a valid file path return dotted_path parts = dotted_path.split('.') if parts[0] == 'chatterbot': parts.pop(0) parts[0] = DATA_DIRECTORY corpus_path = os.path.join(*parts) path_with_extension = '{}.{}'.format(corpus_path, extension) if os.path.exists(path_with_extension): corpus_path = path_with_extension return corpus_path def read_corpus(file_name) -> dict: """ Read and return the data from a corpus json file. """ try: import yaml except ImportError: message = ( 'Unable to import "yaml".\n' 'Please install "pyyaml" to enable chatterbot corpus functionality:\n' 'pip install pyyaml' ) raise OptionalDependencyImportError(message) with io.open(file_name, encoding='utf-8') as data_file: return yaml.safe_load(data_file) def list_corpus_files(dotted_path) -> list[str]: """ Return a list of file paths to each data file in the specified corpus. """ corpus_path = get_file_path(dotted_path, extension=CORPUS_EXTENSION) paths = [] if os.path.isdir(corpus_path): paths = glob.glob(corpus_path + '/**/*.' + CORPUS_EXTENSION, recursive=True) else: paths.append(corpus_path) paths.sort() return paths def load_corpus(*data_file_paths): """ Return the data contained within a specified corpus. """ for file_path in data_file_paths: corpus = [] corpus_data = read_corpus(file_path) conversations = corpus_data.get('conversations', []) corpus.extend(conversations) categories = corpus_data.get('categories', []) yield corpus, categories, file_path ================================================ FILE: chatterbot/exceptions.py ================================================ class OptionalDependencyImportError(ImportError): """ An exception raised when a feature requires an optional dependency to be installed. """ pass ================================================ FILE: chatterbot/ext/__init__.py ================================================ ================================================ FILE: chatterbot/ext/django_chatterbot/__init__.py ================================================ default_app_config = ( 'chatterbot.ext.django_chatterbot.apps.DjangoChatterBotConfig' ) ================================================ FILE: chatterbot/ext/django_chatterbot/abstract_models.py ================================================ from chatterbot.conversation import StatementMixin from chatterbot import constants from django.db import models from django.utils import timezone from django.conf import settings from django.apps import apps DJANGO_APP_NAME = constants.DEFAULT_DJANGO_APP_NAME # Default model paths for swappable models # These can be overridden via CHATTERBOT_STATEMENT_MODEL and CHATTERBOT_TAG_MODEL settings DEFAULT_STATEMENT_MODEL = f'{DJANGO_APP_NAME}.Statement' DEFAULT_TAG_MODEL = f'{DJANGO_APP_NAME}.Tag' class AbstractBaseTag(models.Model): """ The abstract base tag allows other models to be created using the attributes that exist on the default models. """ name = models.SlugField( max_length=constants.TAG_NAME_MAX_LENGTH, unique=True, help_text='The unique name of the tag.' ) class Meta: abstract = True def __str__(self): return self.name class AbstractBaseStatement(models.Model, StatementMixin): """ The abstract base statement allows other models to be created using the attributes that exist on the default models. """ text = models.CharField( max_length=constants.STATEMENT_TEXT_MAX_LENGTH, help_text='The text of the statement.' ) search_text = models.CharField( max_length=constants.STATEMENT_TEXT_MAX_LENGTH, blank=True, help_text='A modified version of the statement text optimized for searching.' ) conversation = models.CharField( max_length=constants.CONVERSATION_LABEL_MAX_LENGTH, help_text='A label used to link this statement to a conversation.' ) created_at = models.DateTimeField( default=timezone.now, help_text='The date and time that the statement was created at.' ) in_response_to = models.CharField( max_length=constants.STATEMENT_TEXT_MAX_LENGTH, null=True, help_text='The text of the statement that this statement is in response to.' ) search_in_response_to = models.CharField( max_length=constants.STATEMENT_TEXT_MAX_LENGTH, blank=True, help_text='A modified version of the in_response_to text optimized for searching.' ) persona = models.CharField( max_length=constants.PERSONA_MAX_LENGTH, help_text='A label used to link this statement to a persona.' ) tags = models.ManyToManyField( settings.CHATTERBOT_TAG_MODEL if hasattr( settings, 'CHATTERBOT_TAG_MODEL' ) else DEFAULT_TAG_MODEL, related_name='statements', help_text='The tags that are associated with this statement.' ) # This is the confidence with which the chat bot believes # this is an accurate response. This value is set when the # statement is returned by the chat bot. confidence = 0 class Meta: abstract = True indexes = [ models.Index( fields=['search_text'], name='idx_cb_search_text' ), models.Index( fields=['search_in_response_to'], name='idx_cb_search_in_response_to' ), ] def __str__(self): if len(self.text.strip()) > 60: return '{}...'.format(self.text[:57]) elif len(self.text.strip()) > 0: return self.text return '' @classmethod def get_tag_model(cls): """ Return the Tag model class, respecting the swappable setting. This method checks: 1. Django settings (CHATTERBOT_TAG_MODEL) - project-wide configuration 2. The model referenced by the 'tags' field - handles custom models via kwargs 3. Falls back to DEFAULT_TAG_MODEL if introspection fails This ensures the correct Tag model is used even when custom models are specified via storage adapter kwargs rather than Django settings. """ tag_model_path = getattr(settings, 'CHATTERBOT_TAG_MODEL', None) if tag_model_path: return apps.get_model(tag_model_path) # If no setting, infer from the ManyToManyField relationship for # cases where custom models are specified via kwargs try: # Get the model that this class's 'tags' field points to tags_field = cls._meta.get_field('tags') related_model = tags_field.related_model # Resolve strings (lazy references) if isinstance(related_model, str): return apps.get_model(related_model) return related_model except Exception: # Fallback to default if introspection fails return apps.get_model(DEFAULT_TAG_MODEL) def get_tags(self) -> list[str]: """ Return the list of tags for this statement. """ return list(self.tags.values_list('name', flat=True)) def add_tags(self, *tags): """ Add a list of strings to the statement as tags. """ TagModel = self.get_tag_model() for tag_name in tags: tag_obj, _created = TagModel.objects.get_or_create(name=tag_name) self.tags.add(tag_obj) ================================================ FILE: chatterbot/ext/django_chatterbot/admin.py ================================================ from django.contrib import admin from chatterbot.ext.django_chatterbot.model_admin import StatementAdmin, TagAdmin from chatterbot.ext.django_chatterbot.models import Statement, Tag admin.site.register(Statement, StatementAdmin) admin.site.register(Tag, TagAdmin) ================================================ FILE: chatterbot/ext/django_chatterbot/apps.py ================================================ from django.apps import AppConfig class DjangoChatterBotConfig(AppConfig): name = 'chatterbot.ext.django_chatterbot' label = 'django_chatterbot' verbose_name = 'Django ChatterBot' def ready(self): from chatterbot.ext.django_chatterbot import settings as defaults from django.conf import settings settings.CHATTERBOT = getattr(settings, 'CHATTERBOT', {}) settings.CHATTERBOT.update(defaults.CHATTERBOT_DEFAULTS) ================================================ FILE: chatterbot/ext/django_chatterbot/migrations/0001_initial.py ================================================ from django.db import migrations, models import django.db.models.deletion class Migration(migrations.Migration): initial = True dependencies = [] operations = [ migrations.CreateModel( name='Response', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('occurrence', models.PositiveIntegerField(default=0)), ], ), migrations.CreateModel( name='Statement', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('text', models.CharField(max_length=255, unique=True)), ], ), migrations.AddField( model_name='response', name='response', field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='+', to='django_chatterbot.Statement'), ), migrations.AddField( model_name='response', name='statement', field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='in_response_to', to='django_chatterbot.Statement'), ), ] ================================================ FILE: chatterbot/ext/django_chatterbot/migrations/0002_statement_extra_data.py ================================================ # Generated by Django 1.10.2 on 2016-10-30 12:13 from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ ('django_chatterbot', '0001_initial'), ] operations = [ migrations.AddField( model_name='statement', name='extra_data', field=models.CharField(default='{}', max_length=500), preserve_default=False, ), ] ================================================ FILE: chatterbot/ext/django_chatterbot/migrations/0003_change_occurrence_default.py ================================================ # Generated by Django 1.9 on 2016-12-12 00:06 from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ ('django_chatterbot', '0002_statement_extra_data'), ] operations = [ migrations.AlterField( model_name='response', name='occurrence', field=models.PositiveIntegerField(default=1), ), ] ================================================ FILE: chatterbot/ext/django_chatterbot/migrations/0004_rename_in_response_to.py ================================================ # Generated by Django 1.10.3 on 2016-12-04 23:52 from django.db import migrations, models import django.db.models.deletion class Migration(migrations.Migration): dependencies = [ ('django_chatterbot', '0003_change_occurrence_default'), ] operations = [ migrations.AlterField( model_name='response', name='statement', field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='in_response', to='django_chatterbot.Statement'), ), migrations.AlterField( model_name='response', name='response', field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='responses', to='django_chatterbot.Statement'), ), ] ================================================ FILE: chatterbot/ext/django_chatterbot/migrations/0005_statement_created_at.py ================================================ # Generated by Django 1.10.1 on 2016-12-29 19:20 from django.db import migrations, models import django.utils.timezone class Migration(migrations.Migration): dependencies = [ ('django_chatterbot', '0004_rename_in_response_to'), ] operations = [ migrations.AddField( model_name='statement', name='created_at', field=models.DateTimeField( default=django.utils.timezone.now, help_text='The date and time that this statement was created at.' ), ), ] ================================================ FILE: chatterbot/ext/django_chatterbot/migrations/0006_create_conversation.py ================================================ # Generated by Django 1.9 on 2017-01-17 07:02 from django.db import migrations, models import django.db.models.deletion import django.utils.timezone class Migration(migrations.Migration): dependencies = [ ('django_chatterbot', '0005_statement_created_at'), ] operations = [ migrations.CreateModel( name='Conversation', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ], ), migrations.AlterField( model_name='statement', name='created_at', field=models.DateTimeField(default=django.utils.timezone.now, help_text='The date and time that this statement was created at.'), ), migrations.AddField( model_name='conversation', name='statements', field=models.ManyToManyField(help_text='The statements in this conversation.', related_name='conversation', to='django_chatterbot.Statement'), ), ] ================================================ FILE: chatterbot/ext/django_chatterbot/migrations/0007_response_created_at.py ================================================ # Generated by Django 1.11 on 2017-07-18 00:16 from django.db import migrations, models import django.utils.timezone class Migration(migrations.Migration): dependencies = [ ('django_chatterbot', '0006_create_conversation'), ] operations = [ migrations.AddField( model_name='response', name='created_at', field=models.DateTimeField( default=django.utils.timezone.now, help_text='The date and time that this response was created at.' ), ), ] ================================================ FILE: chatterbot/ext/django_chatterbot/migrations/0008_update_conversations.py ================================================ # Generated by Django 1.11 on 2017-07-18 11:25 from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ ('django_chatterbot', '0007_response_created_at'), ] operations = [ migrations.RemoveField( model_name='conversation', name='statements', ), migrations.RemoveField( model_name='response', name='occurrence', ), migrations.RemoveField( model_name='statement', name='created_at', ), migrations.AddField( model_name='conversation', name='responses', field=models.ManyToManyField(help_text='The responses in this conversation.', related_name='conversations', to='django_chatterbot.Response'), ), ] ================================================ FILE: chatterbot/ext/django_chatterbot/migrations/0009_tags.py ================================================ # Generated by Django 1.11a1 on 2017-07-07 00:12 from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ ('django_chatterbot', '0008_update_conversations'), ] operations = [ migrations.CreateModel( name='Tag', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('name', models.SlugField()), ], options={ 'abstract': False, }, ), migrations.AlterField( model_name='statement', name='text', field=models.CharField(max_length=255, unique=True), ), migrations.AddField( model_name='tag', name='statements', field=models.ManyToManyField(related_name='tags', to='django_chatterbot.Statement'), ), ] ================================================ FILE: chatterbot/ext/django_chatterbot/migrations/0010_statement_text.py ================================================ # Generated by Django 1.11.4 on 2017-08-16 00:56 from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ ('django_chatterbot', '0009_tags'), ] operations = [ migrations.AlterField( model_name='statement', name='text', field=models.CharField(max_length=400, unique=True), ), ] ================================================ FILE: chatterbot/ext/django_chatterbot/migrations/0011_blank_extra_data.py ================================================ # Generated by Django 1.11.4 on 2017-08-20 13:55 from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ ('django_chatterbot', '0010_statement_text'), ] operations = [ migrations.AlterField( model_name='statement', name='extra_data', field=models.CharField(blank=True, max_length=500), ), ] ================================================ FILE: chatterbot/ext/django_chatterbot/migrations/0012_statement_created_at.py ================================================ from django.db import migrations, models import django.utils.timezone class Migration(migrations.Migration): dependencies = [ ('django_chatterbot', '0011_blank_extra_data'), ] operations = [ migrations.AddField( model_name='statement', name='created_at', field=models.DateTimeField( default=django.utils.timezone.now, help_text='The date and time that the statement was created at.' ), ), ] ================================================ FILE: chatterbot/ext/django_chatterbot/migrations/0013_change_conversations.py ================================================ # Generated by Django 1.11 on 2018-09-13 01:01 from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ ('django_chatterbot', '0012_statement_created_at'), ] operations = [ migrations.RemoveField( model_name='conversation', name='responses', ), migrations.RemoveField( model_name='response', name='response', ), migrations.RemoveField( model_name='response', name='statement', ), migrations.AddField( model_name='statement', name='conversation', field=models.CharField(default='default', max_length=32), preserve_default=False, ), migrations.AddField( model_name='statement', name='in_response_to', field=models.CharField(max_length=400, null=True), ), migrations.AlterField( model_name='statement', name='text', field=models.CharField(max_length=400), ), migrations.DeleteModel( name='Conversation', ), migrations.DeleteModel( name='Response', ), ] ================================================ FILE: chatterbot/ext/django_chatterbot/migrations/0014_remove_statement_extra_data.py ================================================ from django.db import migrations class Migration(migrations.Migration): dependencies = [ ('django_chatterbot', '0013_change_conversations'), ] operations = [ migrations.RemoveField( model_name='statement', name='extra_data', ), ] ================================================ FILE: chatterbot/ext/django_chatterbot/migrations/0015_statement_persona.py ================================================ from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ ('django_chatterbot', '0014_remove_statement_extra_data'), ] operations = [ migrations.AddField( model_name='statement', name='persona', field=models.CharField(default='', max_length=50), preserve_default=False, ), ] ================================================ FILE: chatterbot/ext/django_chatterbot/migrations/0016_statement_stemmed_text.py ================================================ from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ ('django_chatterbot', '0015_statement_persona'), ] operations = [ migrations.AddField( model_name='statement', name='search_text', field=models.CharField(blank=True, max_length=400), ), migrations.AddField( model_name='statement', name='search_in_response_to', field=models.CharField(blank=True, max_length=400), ), ] ================================================ FILE: chatterbot/ext/django_chatterbot/migrations/0017_tags_unique.py ================================================ from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ ('django_chatterbot', '0016_statement_stemmed_text'), ] operations = [ migrations.RemoveField( model_name='tag', name='statements', ), migrations.AddField( model_name='statement', name='tags', field=models.ManyToManyField( related_name='statements', to='django_chatterbot.Tag' ), ), migrations.AlterField( model_name='tag', name='name', field=models.SlugField(unique=True), ), ] ================================================ FILE: chatterbot/ext/django_chatterbot/migrations/0018_text_max_length.py ================================================ from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ ('django_chatterbot', '0017_tags_unique'), ] operations = [ migrations.AlterField( model_name='statement', name='in_response_to', field=models.CharField(max_length=255, null=True), ), migrations.AlterField( model_name='statement', name='search_in_response_to', field=models.CharField(blank=True, max_length=255), ), migrations.AlterField( model_name='statement', name='search_text', field=models.CharField(blank=True, max_length=255), ), migrations.AlterField( model_name='statement', name='text', field=models.CharField(max_length=255), ), ] ================================================ FILE: chatterbot/ext/django_chatterbot/migrations/0019_alter_statement_id_alter_tag_id_and_more.py ================================================ # Generated by Django 4.2.19 on 2025-02-09 13:57 from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ ('django_chatterbot', '0018_text_max_length'), ] operations = [ migrations.AlterField( model_name='statement', name='id', field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), ), migrations.AlterField( model_name='tag', name='id', field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), ), migrations.AddIndex( model_name='statement', index=models.Index(fields=['search_text'], name='idx_cb_search_text'), ), migrations.AddIndex( model_name='statement', index=models.Index(fields=['search_in_response_to'], name='idx_cb_search_in_response_to'), ), ] ================================================ FILE: chatterbot/ext/django_chatterbot/migrations/0020_alter_statement_conversation_and_more.py ================================================ # Generated by Django 4.1 on 2025-03-29 23:27 from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ ('django_chatterbot', '0019_alter_statement_id_alter_tag_id_and_more'), ] operations = [ migrations.AlterField( model_name='statement', name='conversation', field=models.CharField(help_text='A label used to link this statement to a conversation.', max_length=32), ), migrations.AlterField( model_name='statement', name='in_response_to', field=models.CharField(help_text='The text of the statement that this statement is in response to.', max_length=255, null=True), ), migrations.AlterField( model_name='statement', name='persona', field=models.CharField(help_text='A label used to link this statement to a persona.', max_length=50), ), migrations.AlterField( model_name='statement', name='search_in_response_to', field=models.CharField(blank=True, help_text='A modified version of the in_response_to text optimized for searching.', max_length=255), ), migrations.AlterField( model_name='statement', name='search_text', field=models.CharField(blank=True, help_text='A modified version of the statement text optimized for searching.', max_length=255), ), migrations.AlterField( model_name='statement', name='tags', field=models.ManyToManyField(help_text='The tags that are associated with this statement.', related_name='statements', to='django_chatterbot.tag'), ), migrations.AlterField( model_name='statement', name='text', field=models.CharField(help_text='The text of the statement.', max_length=255), ), migrations.AlterField( model_name='tag', name='name', field=models.SlugField(help_text='The unique name of the tag.', unique=True), ), ] ================================================ FILE: chatterbot/ext/django_chatterbot/migrations/0021_increase_text_max_length_to_1100.py ================================================ """ Django migration to increase text field max_length from 255 to 1100. This migration alters all text-related fields in the Statement model: - text - search_text - in_response_to - search_in_response_to This change supports longer conversational statements while remaining within VARCHAR limits for most databases. """ from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ ('django_chatterbot', '0020_alter_statement_conversation_and_more'), ] operations = [ migrations.AlterField( model_name='statement', name='text', field=models.CharField(max_length=1100, help_text='The text of the statement.'), ), migrations.AlterField( model_name='statement', name='search_text', field=models.CharField( blank=True, max_length=1100, help_text='A modified version of the statement text optimized for searching.' ), ), migrations.AlterField( model_name='statement', name='in_response_to', field=models.CharField( max_length=1100, null=True, help_text='The text of the statement that this statement is in response to.' ), ), migrations.AlterField( model_name='statement', name='search_in_response_to', field=models.CharField( blank=True, max_length=1100, help_text='A modified version of the in_response_to text optimized for searching.' ), ), ] ================================================ FILE: chatterbot/ext/django_chatterbot/migrations/__init__.py ================================================ ================================================ FILE: chatterbot/ext/django_chatterbot/model_admin.py ================================================ from django.contrib import admin class StatementAdmin(admin.ModelAdmin): list_display = ('text', 'in_response_to', 'conversation', 'created_at', ) list_filter = ('text', 'created_at', ) search_fields = ('text', ) class TagAdmin(admin.ModelAdmin): list_display = ('name', ) list_filter = ('name', ) search_fields = ('name', ) ================================================ FILE: chatterbot/ext/django_chatterbot/models.py ================================================ from chatterbot.ext.django_chatterbot.abstract_models import AbstractBaseStatement, AbstractBaseTag class Statement(AbstractBaseStatement): """ A statement represents a single spoken entity, sentence or phrase that someone can say. This model can be swapped for a custom model by setting CHATTERBOT_STATEMENT_MODEL in your Django settings. """ class Meta: swappable = 'CHATTERBOT_STATEMENT_MODEL' class Tag(AbstractBaseTag): """ A label that categorizes a statement. This model can be swapped for a custom model by setting CHATTERBOT_TAG_MODEL in your Django settings. """ class Meta: swappable = 'CHATTERBOT_TAG_MODEL' ================================================ FILE: chatterbot/ext/django_chatterbot/settings.py ================================================ """ Default ChatterBot settings for Django. """ from django.conf import settings from chatterbot import constants CHATTERBOT = getattr(settings, 'CHATTERBOT', {}) CHATTERBOT_DEFAULTS = { 'name': 'ChatterBot', 'storage_adapter': 'chatterbot.storage.DjangoStorageAdapter', 'django_app_name': constants.DEFAULT_DJANGO_APP_NAME } CHATTERBOT.update(CHATTERBOT_DEFAULTS) ================================================ FILE: chatterbot/ext/sqlalchemy_app/__init__.py ================================================ ================================================ FILE: chatterbot/ext/sqlalchemy_app/models.py ================================================ from sqlalchemy import Table, Column, Integer, String, DateTime, ForeignKey from sqlalchemy.orm import relationship, declarative_base from sqlalchemy.sql import func from sqlalchemy.ext.declarative import declared_attr from chatterbot.conversation import StatementMixin from chatterbot import constants class ModelBase(object): """ An augmented base class for SqlAlchemy models. """ @declared_attr def __tablename__(cls) -> str: """ Return the lowercase class name as the name of the table. """ return cls.__name__.lower() id = Column( Integer, primary_key=True, autoincrement=True ) Base = declarative_base(cls=ModelBase) tag_association_table = Table( 'tag_association', Base.metadata, Column('tag_id', Integer, ForeignKey('tag.id')), Column('statement_id', Integer, ForeignKey('statement.id')) ) class Tag(Base): """ A tag that describes a statement. """ name = Column( String(constants.TAG_NAME_MAX_LENGTH), unique=True ) class Statement(Base, StatementMixin): """ A Statement represents a sentence or phrase. """ confidence = 0 text = Column( String(constants.STATEMENT_TEXT_MAX_LENGTH) ) search_text = Column( String(constants.STATEMENT_TEXT_MAX_LENGTH), nullable=False, server_default='' ) conversation = Column( String(constants.CONVERSATION_LABEL_MAX_LENGTH), nullable=False, server_default='' ) created_at = Column( DateTime(timezone=True), server_default=func.now() ) tags = relationship( 'Tag', secondary=lambda: tag_association_table, backref='statements' ) in_response_to = Column( String(constants.STATEMENT_TEXT_MAX_LENGTH), nullable=True ) search_in_response_to = Column( String(constants.STATEMENT_TEXT_MAX_LENGTH), nullable=False, server_default='' ) persona = Column( String(constants.PERSONA_MAX_LENGTH), nullable=False, server_default='' ) def get_tags(self) -> list[str]: """ Return a list of tags for this statement. """ return [tag.name for tag in self.tags] def add_tags(self, *tags): """ Add a list of strings to the statement as tags. """ self.tags.extend([ Tag(name=tag) for tag in tags ]) ================================================ FILE: chatterbot/filters.py ================================================ def get_recent_repeated_responses(chatbot, conversation, sample=10, threshold=3, quantity=3) -> list: """ A filter that eliminates possibly repetitive responses to prevent a chat bot from repeating statements that it has recently said. """ from collections import Counter # Get the most recent statements from the conversation conversation_statements = list(chatbot.storage.filter( conversation=conversation, order_by=['id'] ))[sample * -1:] text_of_recent_responses = [ statement.text for statement in conversation_statements ] counter = Counter(text_of_recent_responses) # Find the n most common responses from the conversation most_common = counter.most_common(quantity) return [ counted[0] for counted in most_common if counted[1] >= threshold ] ================================================ FILE: chatterbot/languages.py ================================================ import sys import inspect class AAR: ISO_639_1 = '' ISO_639 = 'aar' ENGLISH_NAME = 'Afar' class ABK: ISO_639_1 = '' ISO_639 = 'abk' ENGLISH_NAME = 'Abkhazian' class ACE: ISO_639_1 = '' ISO_639 = 'ace' ENGLISH_NAME = 'Achinese' class ACH: ISO_639_1 = '' ISO_639 = 'ach' ENGLISH_NAME = 'Acoli' class ADA: ISO_639_1 = '' ISO_639 = 'ada' ENGLISH_NAME = 'Adangme' class ADY: ISO_639_1 = '' ISO_639 = 'ady' ENGLISH_NAME = 'Adyghe' class AFH: ISO_639_1 = '' ISO_639 = 'afh' ENGLISH_NAME = 'Afrihili' class AFR: ISO_639_1 = '' ISO_639 = 'afr' ENGLISH_NAME = 'Afrikaans' class AIN: ISO_639_1 = '' ISO_639 = 'ain' ENGLISH_NAME = 'Ainu' class AKA: ISO_639_1 = '' ISO_639 = 'aka' ENGLISH_NAME = 'Akan' class AKK: ISO_639_1 = '' ISO_639 = 'akk' ENGLISH_NAME = 'Akkadian' class ALB: ISO_639_1 = '' ISO_639 = 'alb' ENGLISH_NAME = 'Albanian' class ALE: ISO_639_1 = '' ISO_639 = 'ale' ENGLISH_NAME = 'Aleut' class ALT: ISO_639_1 = '' ISO_639 = 'alt' ENGLISH_NAME = 'SouthernAltai' class AMH: ISO_639_1 = '' ISO_639 = 'amh' ENGLISH_NAME = 'Amharic' class ANP: ISO_639_1 = '' ISO_639 = 'anp' ENGLISH_NAME = 'Angika' class ARA: ISO_639_1 = '' ISO_639 = 'ara' ENGLISH_NAME = 'Arabic' class ARG: ISO_639_1 = '' ISO_639 = 'arg' ENGLISH_NAME = 'Aragonese' class ARM: ISO_639_1 = '' ISO_639 = 'arm' ENGLISH_NAME = 'Armenian' class ARN: ISO_639_1 = '' ISO_639 = 'arn' ENGLISH_NAME = 'Mapudungun' class ARP: ISO_639_1 = '' ISO_639 = 'arp' ENGLISH_NAME = 'Arapaho' class ARW: ISO_639_1 = '' ISO_639 = 'arw' ENGLISH_NAME = 'Arawak' class ASM: ISO_639_1 = '' ISO_639 = 'asm' ENGLISH_NAME = 'Assamese' class AST: ISO_639_1 = '' ISO_639 = 'ast' ENGLISH_NAME = 'Asturian' class AVA: ISO_639_1 = '' ISO_639 = 'ava' ENGLISH_NAME = 'Avaric' class AVE: ISO_639_1 = '' ISO_639 = 'ave' ENGLISH_NAME = 'Avestan' class AWA: ISO_639_1 = '' ISO_639 = 'awa' ENGLISH_NAME = 'Awadhi' class AYM: ISO_639_1 = '' ISO_639 = 'aym' ENGLISH_NAME = 'Aymara' class AZE: ISO_639_1 = '' ISO_639 = 'aze' ENGLISH_NAME = 'Azerbaijani' class BAK: ISO_639_1 = '' ISO_639 = 'bak' ENGLISH_NAME = 'Bashkir' class BAL: ISO_639_1 = '' ISO_639 = 'bal' ENGLISH_NAME = 'Baluchi' class BAM: ISO_639_1 = '' ISO_639 = 'bam' ENGLISH_NAME = 'Bambara' class BAN: ISO_639_1 = '' ISO_639 = 'ban' ENGLISH_NAME = 'Balinese' class BAQ: ISO_639_1 = '' ISO_639 = 'baq' ENGLISH_NAME = 'Basque' class BAS: ISO_639_1 = '' ISO_639 = 'bas' ENGLISH_NAME = 'Basa' class BEJ: ISO_639_1 = '' ISO_639 = 'bej' ENGLISH_NAME = 'Beja' class BEL: ISO_639_1 = '' ISO_639 = 'bel' ENGLISH_NAME = 'Belarusian' class BEM: ISO_639_1 = '' ISO_639 = 'bem' ENGLISH_NAME = 'Bemba' class BEN: ISO_639_1 = 'bn' ISO_639 = 'ben' ENGLISH_NAME = 'Bengali' class BHO: ISO_639_1 = '' ISO_639 = 'bho' ENGLISH_NAME = 'Bhojpuri' class BIK: ISO_639_1 = '' ISO_639 = 'bik' ENGLISH_NAME = 'Bikol' class BIN: ISO_639_1 = '' ISO_639 = 'bin' ENGLISH_NAME = 'Bini' class BIS: ISO_639_1 = '' ISO_639 = 'bis' ENGLISH_NAME = 'Bislama' class BLA: ISO_639_1 = '' ISO_639 = 'bla' ENGLISH_NAME = 'Siksika' class BOS: ISO_639_1 = '' ISO_639 = 'bos' ENGLISH_NAME = 'Bosnian' class BRA: ISO_639_1 = '' ISO_639 = 'bra' ENGLISH_NAME = 'Braj' class BRE: ISO_639_1 = '' ISO_639 = 'bre' ENGLISH_NAME = 'Breton' class BUA: ISO_639_1 = '' ISO_639 = 'bua' ENGLISH_NAME = 'Buriat' class BUG: ISO_639_1 = '' ISO_639 = 'bug' ENGLISH_NAME = 'Buginese' class BUL: ISO_639_1 = '' ISO_639 = 'bul' ENGLISH_NAME = 'Bulgarian' class BUR: ISO_639_1 = '' ISO_639 = 'bur' ENGLISH_NAME = 'Burmese' class BYN: ISO_639_1 = '' ISO_639 = 'byn' ENGLISH_NAME = 'Blin' class CAD: ISO_639_1 = '' ISO_639 = 'cad' ENGLISH_NAME = 'Caddo' class CAR: ISO_639_1 = '' ISO_639 = 'car' ENGLISH_NAME = 'GalibiCarib' class CAT: ISO_639_1 = '' ISO_639 = 'cat' ENGLISH_NAME = 'Catalan' class CEB: ISO_639_1 = '' ISO_639 = 'ceb' ENGLISH_NAME = 'Cebuano' class CHA: ISO_639_1 = '' ISO_639 = 'cha' ENGLISH_NAME = 'Chamorro' class CHB: ISO_639_1 = '' ISO_639 = 'chb' ENGLISH_NAME = 'Chibcha' class CHE: ISO_639_1 = '' ISO_639 = 'che' ENGLISH_NAME = 'Chechen' class CHG: ISO_639_1 = '' ISO_639 = 'chg' ENGLISH_NAME = 'Chagatai' class CHI: ISO_639_1 = 'zh' ISO_639 = 'chi' ENGLISH_NAME = 'Chinese' class CHK: ISO_639_1 = '' ISO_639 = 'chk' ENGLISH_NAME = 'Chuukese' class CHM: ISO_639_1 = '' ISO_639 = 'chm' ENGLISH_NAME = 'Mari' class CHN: ISO_639_1 = '' ISO_639 = 'chn' ENGLISH_NAME = 'Chinookjargon' class CHO: ISO_639_1 = '' ISO_639 = 'cho' ENGLISH_NAME = 'Choctaw' class CHP: ISO_639_1 = '' ISO_639 = 'chp' ENGLISH_NAME = 'Chipewyan' class CHR: ISO_639_1 = '' ISO_639 = 'chr' ENGLISH_NAME = 'Cherokee' class CHV: ISO_639_1 = '' ISO_639 = 'chv' ENGLISH_NAME = 'Chuvash' class CHY: ISO_639_1 = '' ISO_639 = 'chy' ENGLISH_NAME = 'Cheyenne' class CNR: ISO_639_1 = '' ISO_639 = 'cnr' ENGLISH_NAME = 'Montenegrin' class COP: ISO_639_1 = '' ISO_639 = 'cop' ENGLISH_NAME = 'Coptic' class COR: ISO_639_1 = '' ISO_639 = 'cor' ENGLISH_NAME = 'Cornish' class COS: ISO_639_1 = '' ISO_639 = 'cos' ENGLISH_NAME = 'Corsican' class CPE: ISO_639_1 = '' ISO_639 = 'cpe' ENGLISH_NAME = 'Creolesandpidgins' class CPF: ISO_639_1 = '' ISO_639 = 'cpf' ENGLISH_NAME = 'Creolesandpidgins' class CPP: ISO_639_1 = '' ISO_639 = 'cpp' ENGLISH_NAME = 'Creolesandpidgins' class CRE: ISO_639_1 = '' ISO_639 = 'cre' ENGLISH_NAME = 'Cree' class CRH: ISO_639_1 = '' ISO_639 = 'crh' ENGLISH_NAME = 'CrimeanTatar' class CRP: ISO_639_1 = '' ISO_639 = 'crp' ENGLISH_NAME = 'Creolesandpidgins' class CSB: ISO_639_1 = '' ISO_639 = 'csb' ENGLISH_NAME = 'Kashubian' class CZE: ISO_639_1 = '' ISO_639 = 'cze' ENGLISH_NAME = 'Czech' class DAK: ISO_639_1 = '' ISO_639 = 'dak' ENGLISH_NAME = 'Dakota' class DAN: ISO_639_1 = '' ISO_639 = 'dan' ENGLISH_NAME = 'Danish' class DAR: ISO_639_1 = '' ISO_639 = 'dar' ENGLISH_NAME = 'Dargwa' class DEL: ISO_639_1 = '' ISO_639 = 'del' ENGLISH_NAME = 'Delaware' class DEN: ISO_639_1 = '' ISO_639 = 'den' ENGLISH_NAME = 'Slave' class DGR: ISO_639_1 = '' ISO_639 = 'dgr' ENGLISH_NAME = 'Dogrib' class DIN: ISO_639_1 = '' ISO_639 = 'din' ENGLISH_NAME = 'Dinka' class DIV: ISO_639_1 = '' ISO_639 = 'div' ENGLISH_NAME = 'Divehi' class DOI: ISO_639_1 = '' ISO_639 = 'doi' ENGLISH_NAME = 'Dogri' class DUA: ISO_639_1 = '' ISO_639 = 'dua' ENGLISH_NAME = 'Duala' class DUT: ISO_639_1 = 'nl' ISO_639 = 'dut' ENGLISH_NAME = 'Dutch' class DYU: ISO_639_1 = '' ISO_639 = 'dyu' ENGLISH_NAME = 'Dyula' class DZO: ISO_639_1 = '' ISO_639 = 'dzo' ENGLISH_NAME = 'Dzongkha' class EFI: ISO_639_1 = '' ISO_639 = 'efi' ENGLISH_NAME = 'Efik' class EKA: ISO_639_1 = '' ISO_639 = 'eka' ENGLISH_NAME = 'Ekajuk' class ELX: ISO_639_1 = '' ISO_639 = 'elx' ENGLISH_NAME = 'Elamite' class ENG: ISO_639_1 = 'en' ISO_639 = 'eng' ENGLISH_NAME = 'English' class EPO: ISO_639_1 = '' ISO_639 = 'epo' ENGLISH_NAME = 'Esperanto' class EST: ISO_639_1 = '' ISO_639 = 'est' ENGLISH_NAME = 'Estonian' class EWE: ISO_639_1 = '' ISO_639 = 'ewe' ENGLISH_NAME = 'Ewe' class EWO: ISO_639_1 = '' ISO_639 = 'ewo' ENGLISH_NAME = 'Ewondo' class FAN: ISO_639_1 = '' ISO_639 = 'fan' ENGLISH_NAME = 'Fang' class FAO: ISO_639_1 = '' ISO_639 = 'fao' ENGLISH_NAME = 'Faroese' class FAT: ISO_639_1 = '' ISO_639 = 'fat' ENGLISH_NAME = 'Fanti' class FIJ: ISO_639_1 = '' ISO_639 = 'fij' ENGLISH_NAME = 'Fijian' class FIL: ISO_639_1 = '' ISO_639 = 'fil' ENGLISH_NAME = 'Filipino' class FIN: ISO_639_1 = '' ISO_639 = 'fin' ENGLISH_NAME = 'Finnish' class FON: ISO_639_1 = '' ISO_639 = 'fon' ENGLISH_NAME = 'Fon' class FRE: ISO_639_1 = '' ISO_639 = 'fre' ENGLISH_NAME = 'French' class FRR: ISO_639_1 = '' ISO_639 = 'frr' ENGLISH_NAME = 'NorthernFrisian' class FRS: ISO_639_1 = '' ISO_639 = 'frs' ENGLISH_NAME = 'EasternFrisian' class FRY: ISO_639_1 = '' ISO_639 = 'fry' ENGLISH_NAME = 'WesternFrisian' class FUL: ISO_639_1 = '' ISO_639 = 'ful' ENGLISH_NAME = 'Fulah' class FUR: ISO_639_1 = '' ISO_639 = 'fur' ENGLISH_NAME = 'Friulian' class GAA: ISO_639_1 = '' ISO_639 = 'gaa' ENGLISH_NAME = 'Ga' class GAY: ISO_639_1 = '' ISO_639 = 'gay' ENGLISH_NAME = 'Gayo' class GBA: ISO_639_1 = '' ISO_639 = 'gba' ENGLISH_NAME = 'Gbaya' class GEO: ISO_639_1 = '' ISO_639 = 'geo' ENGLISH_NAME = 'Georgian' class GER: ISO_639_1 = 'de' ISO_639 = 'ger' ENGLISH_NAME = 'German' class GEZ: ISO_639_1 = '' ISO_639 = 'gez' ENGLISH_NAME = 'Geez' class GIL: ISO_639_1 = '' ISO_639 = 'gil' ENGLISH_NAME = 'Gilbertese' class GLA: ISO_639_1 = '' ISO_639 = 'gla' ENGLISH_NAME = 'Gaelic' class GLE: ISO_639_1 = '' ISO_639 = 'gle' ENGLISH_NAME = 'Irish' class GLG: ISO_639_1 = '' ISO_639 = 'glg' ENGLISH_NAME = 'Galician' class GLV: ISO_639_1 = '' ISO_639 = 'glv' ENGLISH_NAME = 'Manx' class GON: ISO_639_1 = '' ISO_639 = 'gon' ENGLISH_NAME = 'Gondi' class GOR: ISO_639_1 = '' ISO_639 = 'gor' ENGLISH_NAME = 'Gorontalo' class GOT: ISO_639_1 = '' ISO_639 = 'got' ENGLISH_NAME = 'Gothic' class GRB: ISO_639_1 = '' ISO_639 = 'grb' ENGLISH_NAME = 'Grebo' class GRE: ISO_639_1 = 'el' ISO_639 = 'gre' ENGLISH_NAME = 'Greek' class GRN: ISO_639_1 = '' ISO_639 = 'grn' ENGLISH_NAME = 'Guarani' class GSW: ISO_639_1 = '' ISO_639 = 'gsw' ENGLISH_NAME = 'SwissGerman' class GUJ: ISO_639_1 = '' ISO_639 = 'guj' ENGLISH_NAME = 'Gujarati' class GWI: ISO_639_1 = '' ISO_639 = 'gwi' ENGLISH_NAME = 'Gwichin' class HAI: ISO_639_1 = '' ISO_639 = 'hai' ENGLISH_NAME = 'Haida' class HAT: ISO_639_1 = '' ISO_639 = 'hat' ENGLISH_NAME = 'Haitian' class HAU: ISO_639_1 = '' ISO_639 = 'hau' ENGLISH_NAME = 'Hausa' class HAW: ISO_639_1 = '' ISO_639 = 'haw' ENGLISH_NAME = 'Hawaiian' class HEB: ISO_639_1 = 'he' ISO_639 = 'heb' ENGLISH_NAME = 'Hebrew' class HER: ISO_639_1 = '' ISO_639 = 'her' ENGLISH_NAME = 'Herero' class HIL: ISO_639_1 = '' ISO_639 = 'hil' ENGLISH_NAME = 'Hiligaynon' class HIN: ISO_639_1 = 'hi' ISO_639 = 'hin' ENGLISH_NAME = 'Hindi' class HIT: ISO_639_1 = '' ISO_639 = 'hit' ENGLISH_NAME = 'Hittite' class HMN: ISO_639_1 = '' ISO_639 = 'hmn' ENGLISH_NAME = 'Hmong' class HMO: ISO_639_1 = '' ISO_639 = 'hmo' ENGLISH_NAME = 'HiriMotu' class HRV: ISO_639_1 = '' ISO_639 = 'hrv' ENGLISH_NAME = 'Croatian' class HSB: ISO_639_1 = '' ISO_639 = 'hsb' ENGLISH_NAME = 'UpperSorbian' class HUN: ISO_639_1 = '' ISO_639 = 'hun' ENGLISH_NAME = 'Hungarian' class HUP: ISO_639_1 = '' ISO_639 = 'hup' ENGLISH_NAME = 'Hupa' class IBA: ISO_639_1 = '' ISO_639 = 'iba' ENGLISH_NAME = 'Iban' class IBO: ISO_639_1 = '' ISO_639 = 'ibo' ENGLISH_NAME = 'Igbo' class ICE: ISO_639_1 = '' ISO_639 = 'ice' ENGLISH_NAME = 'Icelandic' class IDO: ISO_639_1 = '' ISO_639 = 'ido' ENGLISH_NAME = 'Ido' class III: ISO_639_1 = '' ISO_639 = 'iii' ENGLISH_NAME = 'SichuanYi' class IKU: ISO_639_1 = '' ISO_639 = 'iku' ENGLISH_NAME = 'Inuktitut' class ILE: ISO_639_1 = '' ISO_639 = 'ile' ENGLISH_NAME = 'Interlingue' class ILO: ISO_639_1 = '' ISO_639 = 'ilo' ENGLISH_NAME = 'Iloko' class INA: ISO_639_1 = '' ISO_639 = 'ina' ENGLISH_NAME = 'Interlingua' class IND: ISO_639_1 = 'id' ISO_639 = 'ind' ENGLISH_NAME = 'Indonesian' class INH: ISO_639_1 = '' ISO_639 = 'inh' ENGLISH_NAME = 'Ingush' class IPK: ISO_639_1 = '' ISO_639 = 'ipk' ENGLISH_NAME = 'Inupiaq' class ITA: ISO_639_1 = '' ISO_639 = 'ita' ENGLISH_NAME = 'Italian' class JAV: ISO_639_1 = '' ISO_639 = 'jav' ENGLISH_NAME = 'Javanese' class JBO: ISO_639_1 = '' ISO_639 = 'jbo' ENGLISH_NAME = 'Lojban' class JPN: ISO_639_1 = 'ja' ISO_639 = 'jpn' ENGLISH_NAME = 'Japanese' class JPR: ISO_639_1 = '' ISO_639 = 'jpr' ENGLISH_NAME = 'JudeoPersian' class JRB: ISO_639_1 = '' ISO_639 = 'jrb' ENGLISH_NAME = 'JudeoArabic' class KAA: ISO_639_1 = '' ISO_639 = 'kaa' ENGLISH_NAME = 'KaraKalpak' class KAB: ISO_639_1 = '' ISO_639 = 'kab' ENGLISH_NAME = 'Kabyle' class KAC: ISO_639_1 = '' ISO_639 = 'kac' ENGLISH_NAME = 'Kachin' class KAL: ISO_639_1 = '' ISO_639 = 'kal' ENGLISH_NAME = 'Kalaallisut' class KAM: ISO_639_1 = '' ISO_639 = 'kam' ENGLISH_NAME = 'Kamba' class KAN: ISO_639_1 = '' ISO_639 = 'kan' ENGLISH_NAME = 'Kannada' class KAS: ISO_639_1 = '' ISO_639 = 'kas' ENGLISH_NAME = 'Kashmiri' class KAU: ISO_639_1 = '' ISO_639 = 'kau' ENGLISH_NAME = 'Kanuri' class KAW: ISO_639_1 = '' ISO_639 = 'kaw' ENGLISH_NAME = 'Kawi' class KAZ: ISO_639_1 = '' ISO_639 = 'kaz' ENGLISH_NAME = 'Kazakh' class KBD: ISO_639_1 = '' ISO_639 = 'kbd' ENGLISH_NAME = 'Kabardian' class KHA: ISO_639_1 = '' ISO_639 = 'kha' ENGLISH_NAME = 'Khasi' class KHM: ISO_639_1 = '' ISO_639 = 'khm' ENGLISH_NAME = 'CentralKhmer' class KHO: ISO_639_1 = '' ISO_639 = 'kho' ENGLISH_NAME = 'Khotanese' class KIK: ISO_639_1 = '' ISO_639 = 'kik' ENGLISH_NAME = 'Kikuyu' class KIN: ISO_639_1 = '' ISO_639 = 'kin' ENGLISH_NAME = 'Kinyarwanda' class KIR: ISO_639_1 = '' ISO_639 = 'kir' ENGLISH_NAME = 'Kirghiz' class KMB: ISO_639_1 = '' ISO_639 = 'kmb' ENGLISH_NAME = 'Kimbundu' class KOK: ISO_639_1 = '' ISO_639 = 'kok' ENGLISH_NAME = 'Konkani' class KOM: ISO_639_1 = '' ISO_639 = 'kom' ENGLISH_NAME = 'Komi' class KON: ISO_639_1 = '' ISO_639 = 'kon' ENGLISH_NAME = 'Kongo' class KOR: ISO_639_1 = 'ko' ISO_639 = 'kor' ENGLISH_NAME = 'Korean' class KOS: ISO_639_1 = '' ISO_639 = 'kos' ENGLISH_NAME = 'Kosraean' class KPE: ISO_639_1 = '' ISO_639 = 'kpe' ENGLISH_NAME = 'Kpelle' class KRC: ISO_639_1 = '' ISO_639 = 'krc' ENGLISH_NAME = 'KarachayBalkar' class KRL: ISO_639_1 = '' ISO_639 = 'krl' ENGLISH_NAME = 'Karelian' class KRU: ISO_639_1 = '' ISO_639 = 'kru' ENGLISH_NAME = 'Kurukh' class KUA: ISO_639_1 = '' ISO_639 = 'kua' ENGLISH_NAME = 'Kuanyama' class KUM: ISO_639_1 = '' ISO_639 = 'kum' ENGLISH_NAME = 'Kumyk' class KUR: ISO_639_1 = '' ISO_639 = 'kur' ENGLISH_NAME = 'Kurdish' class KUT: ISO_639_1 = '' ISO_639 = 'kut' ENGLISH_NAME = 'Kutenai' class LAD: ISO_639_1 = '' ISO_639 = 'lad' ENGLISH_NAME = 'Ladino' class LAH: ISO_639_1 = '' ISO_639 = 'lah' ENGLISH_NAME = 'Lahnda' class LAM: ISO_639_1 = '' ISO_639 = 'lam' ENGLISH_NAME = 'Lamba' class LAO: ISO_639_1 = '' ISO_639 = 'lao' ENGLISH_NAME = 'Lao' class LAT: ISO_639_1 = '' ISO_639 = 'lat' ENGLISH_NAME = 'Latin' class LAV: ISO_639_1 = '' ISO_639 = 'lav' ENGLISH_NAME = 'Latvian' class LEZ: ISO_639_1 = '' ISO_639 = 'lez' ENGLISH_NAME = 'Lezghian' class LIM: ISO_639_1 = '' ISO_639 = 'lim' ENGLISH_NAME = 'Limburgan' class LIN: ISO_639_1 = '' ISO_639 = 'lin' ENGLISH_NAME = 'Lingala' class LIT: ISO_639_1 = '' ISO_639 = 'lit' ENGLISH_NAME = 'Lithuanian' class LOL: ISO_639_1 = '' ISO_639 = 'lol' ENGLISH_NAME = 'Mongo' class LOZ: ISO_639_1 = '' ISO_639 = 'loz' ENGLISH_NAME = 'Lozi' class LTZ: ISO_639_1 = '' ISO_639 = 'ltz' ENGLISH_NAME = 'Luxembourgish' class LUA: ISO_639_1 = '' ISO_639 = 'lua' ENGLISH_NAME = 'LubaLulua' class LUB: ISO_639_1 = '' ISO_639 = 'lub' ENGLISH_NAME = 'LubaKatanga' class LUG: ISO_639_1 = '' ISO_639 = 'lug' ENGLISH_NAME = 'Ganda' class LUI: ISO_639_1 = '' ISO_639 = 'lui' ENGLISH_NAME = 'Luiseno' class LUN: ISO_639_1 = '' ISO_639 = 'lun' ENGLISH_NAME = 'Lunda' class LUO: ISO_639_1 = '' ISO_639 = 'luo' ENGLISH_NAME = 'Luo' class LUS: ISO_639_1 = '' ISO_639 = 'lus' ENGLISH_NAME = 'Lushai' class MAC: ISO_639_1 = '' ISO_639 = 'mac' ENGLISH_NAME = 'Macedonian' class MAD: ISO_639_1 = '' ISO_639 = 'mad' ENGLISH_NAME = 'Madurese' class MAG: ISO_639_1 = '' ISO_639 = 'mag' ENGLISH_NAME = 'Magahi' class MAH: ISO_639_1 = '' ISO_639 = 'mah' ENGLISH_NAME = 'Marshallese' class MAI: ISO_639_1 = '' ISO_639 = 'mai' ENGLISH_NAME = 'Maithili' class MAK: ISO_639_1 = '' ISO_639 = 'mak' ENGLISH_NAME = 'Makasar' class MAL: ISO_639_1 = '' ISO_639 = 'mal' ENGLISH_NAME = 'Malayalam' class MAN: ISO_639_1 = '' ISO_639 = 'man' ENGLISH_NAME = 'Mandingo' class MAO: ISO_639_1 = '' ISO_639 = 'mao' ENGLISH_NAME = 'Maori' class MAR: ISO_639_1 = 'mr' ISO_639 = 'mar' ENGLISH_NAME = 'Marathi' class MAS: ISO_639_1 = '' ISO_639 = 'mas' ENGLISH_NAME = 'Masai' class MAY: ISO_639_1 = '' ISO_639 = 'may' ENGLISH_NAME = 'Malay' class MDF: ISO_639_1 = '' ISO_639 = 'mdf' ENGLISH_NAME = 'Moksha' class MDR: ISO_639_1 = '' ISO_639 = 'mdr' ENGLISH_NAME = 'Mandar' class MEN: ISO_639_1 = '' ISO_639 = 'men' ENGLISH_NAME = 'Mende' class MIC: ISO_639_1 = '' ISO_639 = 'mic' ENGLISH_NAME = 'Mikmaq' class MIN: ISO_639_1 = '' ISO_639 = 'min' ENGLISH_NAME = 'Minangkabau' class MLG: ISO_639_1 = '' ISO_639 = 'mlg' ENGLISH_NAME = 'Malagasy' class MLT: ISO_639_1 = '' ISO_639 = 'mlt' ENGLISH_NAME = 'Maltese' class MNC: ISO_639_1 = '' ISO_639 = 'mnc' ENGLISH_NAME = 'Manchu' class MNI: ISO_639_1 = '' ISO_639 = 'mni' ENGLISH_NAME = 'Manipuri' class MOH: ISO_639_1 = '' ISO_639 = 'moh' ENGLISH_NAME = 'Mohawk' class MON: ISO_639_1 = '' ISO_639 = 'mon' ENGLISH_NAME = 'Mongolian' class MOS: ISO_639_1 = '' ISO_639 = 'mos' ENGLISH_NAME = 'Mossi' class MUS: ISO_639_1 = '' ISO_639 = 'mus' ENGLISH_NAME = 'Creek' class MWL: ISO_639_1 = '' ISO_639 = 'mwl' ENGLISH_NAME = 'Mirandese' class MWR: ISO_639_1 = '' ISO_639 = 'mwr' ENGLISH_NAME = 'Marwari' class MYV: ISO_639_1 = '' ISO_639 = 'myv' ENGLISH_NAME = 'Erzya' class NAP: ISO_639_1 = '' ISO_639 = 'nap' ENGLISH_NAME = 'Neapolitan' class NAU: ISO_639_1 = '' ISO_639 = 'nau' ENGLISH_NAME = 'Nauru' class NAV: ISO_639_1 = '' ISO_639 = 'nav' ENGLISH_NAME = 'Navajo' class NBL: ISO_639_1 = '' ISO_639 = 'nbl' ENGLISH_NAME = 'Ndebele' class NDE: ISO_639_1 = '' ISO_639 = 'nde' ENGLISH_NAME = 'Ndebele' class NDO: ISO_639_1 = '' ISO_639 = 'ndo' ENGLISH_NAME = 'Ndonga' class NEP: ISO_639_1 = '' ISO_639 = 'nep' ENGLISH_NAME = 'Nepali' class NEW: ISO_639_1 = '' ISO_639 = 'new' ENGLISH_NAME = 'NepalBhasa' class NIA: ISO_639_1 = '' ISO_639 = 'nia' ENGLISH_NAME = 'Nias' class NIU: ISO_639_1 = '' ISO_639 = 'niu' ENGLISH_NAME = 'Niuean' class NNO: ISO_639_1 = '' ISO_639 = 'nno' ENGLISH_NAME = 'NorwegianNynorsk' class NOB: ISO_639_1 = '' ISO_639 = 'nob' ENGLISH_NAME = 'Bokmål' class NOG: ISO_639_1 = '' ISO_639 = 'nog' ENGLISH_NAME = 'Nogai' class NOR: ISO_639_1 = '' ISO_639 = 'nor' ENGLISH_NAME = 'Norwegian' class NQO: ISO_639_1 = '' ISO_639 = 'nqo' ENGLISH_NAME = 'NKo' class NSO: ISO_639_1 = '' ISO_639 = 'nso' ENGLISH_NAME = 'Pedi' class NYA: ISO_639_1 = '' ISO_639 = 'nya' ENGLISH_NAME = 'Chichewa' class NYM: ISO_639_1 = '' ISO_639 = 'nym' ENGLISH_NAME = 'Nyamwezi' class NYN: ISO_639_1 = '' ISO_639 = 'nyn' ENGLISH_NAME = 'Nyankole' class NYO: ISO_639_1 = '' ISO_639 = 'nyo' ENGLISH_NAME = 'Nyoro' class NZI: ISO_639_1 = '' ISO_639 = 'nzi' ENGLISH_NAME = 'Nzima' class OJI: ISO_639_1 = '' ISO_639 = 'oji' ENGLISH_NAME = 'Ojibwa' class ORI: ISO_639_1 = 'or' ISO_639 = 'ori' ENGLISH_NAME = 'Oriya' class ORM: ISO_639_1 = '' ISO_639 = 'orm' ENGLISH_NAME = 'Oromo' class OSA: ISO_639_1 = '' ISO_639 = 'osa' ENGLISH_NAME = 'Osage' class OSS: ISO_639_1 = '' ISO_639 = 'oss' ENGLISH_NAME = 'Ossetian' class PAG: ISO_639_1 = '' ISO_639 = 'pag' ENGLISH_NAME = 'Pangasinan' class PAL: ISO_639_1 = '' ISO_639 = 'pal' ENGLISH_NAME = 'Pahlavi' class PAM: ISO_639_1 = '' ISO_639 = 'pam' ENGLISH_NAME = 'Pampanga' class PAN: ISO_639_1 = '' ISO_639 = 'pan' ENGLISH_NAME = 'Panjabi' class PAP: ISO_639_1 = '' ISO_639 = 'pap' ENGLISH_NAME = 'Papiamento' class PAU: ISO_639_1 = '' ISO_639 = 'pau' ENGLISH_NAME = 'Palauan' class PER: ISO_639_1 = 'fa' ISO_639 = 'per' ENGLISH_NAME = 'Persian' class PHN: ISO_639_1 = '' ISO_639 = 'phn' ENGLISH_NAME = 'Phoenician' class PLI: ISO_639_1 = '' ISO_639 = 'pli' ENGLISH_NAME = 'Pali' class POL: ISO_639_1 = '' ISO_639 = 'pol' ENGLISH_NAME = 'Polish' class PON: ISO_639_1 = '' ISO_639 = 'pon' ENGLISH_NAME = 'Pohnpeian' class POR: ISO_639_1 = 'pt' ISO_639 = 'por' ENGLISH_NAME = 'Portuguese' class PUS: ISO_639_1 = '' ISO_639 = 'pus' ENGLISH_NAME = 'Pushto' class QUE: ISO_639_1 = '' ISO_639 = 'que' ENGLISH_NAME = 'Quechua' class RAJ: ISO_639_1 = '' ISO_639 = 'raj' ENGLISH_NAME = 'Rajasthani' class RAP: ISO_639_1 = '' ISO_639 = 'rap' ENGLISH_NAME = 'Rapanui' class RAR: ISO_639_1 = '' ISO_639 = 'rar' ENGLISH_NAME = 'Rarotongan' class ROH: ISO_639_1 = '' ISO_639 = 'roh' ENGLISH_NAME = 'Romansh' class ROM: ISO_639_1 = '' ISO_639 = 'rom' ENGLISH_NAME = 'Romany' class RUM: ISO_639_1 = '' ISO_639 = 'rum' ENGLISH_NAME = 'Romanian' class RUN: ISO_639_1 = '' ISO_639 = 'run' ENGLISH_NAME = 'Rundi' class RUP: ISO_639_1 = '' ISO_639 = 'rup' ENGLISH_NAME = 'Aromanian' class RUS: ISO_639_1 = 'ru' ISO_639 = 'rus' ENGLISH_NAME = 'Russian' class SAD: ISO_639_1 = '' ISO_639 = 'sad' ENGLISH_NAME = 'Sandawe' class SAG: ISO_639_1 = '' ISO_639 = 'sag' ENGLISH_NAME = 'Sango' class SAH: ISO_639_1 = '' ISO_639 = 'sah' ENGLISH_NAME = 'Yakut' class SAM: ISO_639_1 = '' ISO_639 = 'sam' ENGLISH_NAME = 'SamaritanAramaic' class SAN: ISO_639_1 = '' ISO_639 = 'san' ENGLISH_NAME = 'Sanskrit' class SAS: ISO_639_1 = '' ISO_639 = 'sas' ENGLISH_NAME = 'Sasak' class SAT: ISO_639_1 = '' ISO_639 = 'sat' ENGLISH_NAME = 'Santali' class SCN: ISO_639_1 = '' ISO_639 = 'scn' ENGLISH_NAME = 'Sicilian' class SCO: ISO_639_1 = '' ISO_639 = 'sco' ENGLISH_NAME = 'Scots' class SEL: ISO_639_1 = '' ISO_639 = 'sel' ENGLISH_NAME = 'Selkup' class SHN: ISO_639_1 = '' ISO_639 = 'shn' ENGLISH_NAME = 'Shan' class SID: ISO_639_1 = '' ISO_639 = 'sid' ENGLISH_NAME = 'Sidamo' class SIN: ISO_639_1 = '' ISO_639 = 'sin' ENGLISH_NAME = 'Sinhala' class SLO: ISO_639_1 = '' ISO_639 = 'slo' ENGLISH_NAME = 'Slovak' class SLV: ISO_639_1 = '' ISO_639 = 'slv' ENGLISH_NAME = 'Slovenian' class SMA: ISO_639_1 = '' ISO_639 = 'sma' ENGLISH_NAME = 'SouthernSami' class SME: ISO_639_1 = '' ISO_639 = 'sme' ENGLISH_NAME = 'NorthernSami' class SMJ: ISO_639_1 = '' ISO_639 = 'smj' ENGLISH_NAME = 'LuleSami' class SMN: ISO_639_1 = '' ISO_639 = 'smn' ENGLISH_NAME = 'InariSami' class SMO: ISO_639_1 = '' ISO_639 = 'smo' ENGLISH_NAME = 'Samoan' class SMS: ISO_639_1 = '' ISO_639 = 'sms' ENGLISH_NAME = 'SkoltSami' class SNA: ISO_639_1 = '' ISO_639 = 'sna' ENGLISH_NAME = 'Shona' class SND: ISO_639_1 = '' ISO_639 = 'snd' ENGLISH_NAME = 'Sindhi' class SNK: ISO_639_1 = '' ISO_639 = 'snk' ENGLISH_NAME = 'Soninke' class SOG: ISO_639_1 = '' ISO_639 = 'sog' ENGLISH_NAME = 'Sogdian' class SOM: ISO_639_1 = '' ISO_639 = 'som' ENGLISH_NAME = 'Somali' class SOT: ISO_639_1 = '' ISO_639 = 'sot' ENGLISH_NAME = 'Sotho' class SPA: ISO_639_1 = 'es' ISO_639 = 'spa' ENGLISH_NAME = 'Spanish' class SRD: ISO_639_1 = '' ISO_639 = 'srd' ENGLISH_NAME = 'Sardinian' class SRN: ISO_639_1 = '' ISO_639 = 'srn' ENGLISH_NAME = 'SrananTongo' class SRP: ISO_639_1 = '' ISO_639 = 'srp' ENGLISH_NAME = 'Serbian' class SRR: ISO_639_1 = '' ISO_639 = 'srr' ENGLISH_NAME = 'Serer' class SSW: ISO_639_1 = '' ISO_639 = 'ssw' ENGLISH_NAME = 'Swati' class SUK: ISO_639_1 = '' ISO_639 = 'suk' ENGLISH_NAME = 'Sukuma' class SUN: ISO_639_1 = '' ISO_639 = 'sun' ENGLISH_NAME = 'Sundanese' class SUS: ISO_639_1 = '' ISO_639 = 'sus' ENGLISH_NAME = 'Susu' class SUX: ISO_639_1 = '' ISO_639 = 'sux' ENGLISH_NAME = 'Sumerian' class SWA: ISO_639_1 = '' ISO_639 = 'swa' ENGLISH_NAME = 'Swahili' class SWE: ISO_639_1 = 'sv' ISO_639 = 'swe' ENGLISH_NAME = 'Swedish' class SYC: ISO_639_1 = '' ISO_639 = 'syc' ENGLISH_NAME = 'ClassicalSyriac' class SYR: ISO_639_1 = '' ISO_639 = 'syr' ENGLISH_NAME = 'Syriac' class TAH: ISO_639_1 = 'th' ISO_639 = 'tah' ENGLISH_NAME = 'Tahitian' class TAM: ISO_639_1 = '' ISO_639 = 'tam' ENGLISH_NAME = 'Tamil' class TAT: ISO_639_1 = '' ISO_639 = 'tat' ENGLISH_NAME = 'Tatar' class TEL: ISO_639_1 = 'te' ISO_639 = 'tel' ENGLISH_NAME = 'Telugu' class TEM: ISO_639_1 = '' ISO_639 = 'tem' ENGLISH_NAME = 'Timne' class TER: ISO_639_1 = '' ISO_639 = 'ter' ENGLISH_NAME = 'Tereno' class TET: ISO_639_1 = '' ISO_639 = 'tet' ENGLISH_NAME = 'Tetum' class TGK: ISO_639_1 = '' ISO_639 = 'tgk' ENGLISH_NAME = 'Tajik' class TGL: ISO_639_1 = '' ISO_639 = 'tgl' ENGLISH_NAME = 'Tagalog' class THA: ISO_639_1 = '' ISO_639 = 'tha' ENGLISH_NAME = 'Thai' class TIB: ISO_639_1 = '' ISO_639 = 'tib' ENGLISH_NAME = 'Tibetan' class TIG: ISO_639_1 = '' ISO_639 = 'tig' ENGLISH_NAME = 'Tigre' class TIR: ISO_639_1 = '' ISO_639 = 'tir' ENGLISH_NAME = 'Tigrinya' class TIV: ISO_639_1 = '' ISO_639 = 'tiv' ENGLISH_NAME = 'Tiv' class TKL: ISO_639_1 = '' ISO_639 = 'tkl' ENGLISH_NAME = 'Tokelau' class TLH: ISO_639_1 = '' ISO_639 = 'tlh' ENGLISH_NAME = 'Klingon' class TLI: ISO_639_1 = '' ISO_639 = 'tli' ENGLISH_NAME = 'Tlingit' class TMH: ISO_639_1 = '' ISO_639 = 'tmh' ENGLISH_NAME = 'Tamashek' class TOG: ISO_639_1 = '' ISO_639 = 'tog' ENGLISH_NAME = 'Tonga' class TON: ISO_639_1 = '' ISO_639 = 'ton' ENGLISH_NAME = 'Tonga' class TPI: ISO_639_1 = '' ISO_639 = 'tpi' ENGLISH_NAME = 'TokPisin' class TSI: ISO_639_1 = '' ISO_639 = 'tsi' ENGLISH_NAME = 'Tsimshian' class TSN: ISO_639_1 = '' ISO_639 = 'tsn' ENGLISH_NAME = 'Tswana' class TSO: ISO_639_1 = '' ISO_639 = 'tso' ENGLISH_NAME = 'Tsonga' class TUK: ISO_639_1 = '' ISO_639 = 'tuk' ENGLISH_NAME = 'Turkmen' class TUM: ISO_639_1 = '' ISO_639 = 'tum' ENGLISH_NAME = 'Tumbuka' class TUR: ISO_639_1 = '' ISO_639 = 'tur' ENGLISH_NAME = 'Turkish' class TVL: ISO_639_1 = '' ISO_639 = 'tvl' ENGLISH_NAME = 'Tuvalu' class TWI: ISO_639_1 = '' ISO_639 = 'twi' ENGLISH_NAME = 'Twi' class TYV: ISO_639_1 = '' ISO_639 = 'tyv' ENGLISH_NAME = 'Tuvinian' class UDM: ISO_639_1 = '' ISO_639 = 'udm' ENGLISH_NAME = 'Udmurt' class UGA: ISO_639_1 = '' ISO_639 = 'uga' ENGLISH_NAME = 'Ugaritic' class UIG: ISO_639_1 = '' ISO_639 = 'uig' ENGLISH_NAME = 'Uighur' class UKR: ISO_639_1 = '' ISO_639 = 'ukr' ENGLISH_NAME = 'Ukrainian' class UMB: ISO_639_1 = '' ISO_639 = 'umb' ENGLISH_NAME = 'Umbundu' class UND: ISO_639_1 = '' ISO_639 = 'und' ENGLISH_NAME = 'Undetermined' class URD: ISO_639_1 = '' ISO_639 = 'urd' ENGLISH_NAME = 'Urdu' class UZB: ISO_639_1 = '' ISO_639 = 'uzb' ENGLISH_NAME = 'Uzbek' class VAI: ISO_639_1 = '' ISO_639 = 'vai' ENGLISH_NAME = 'Vai' class VEN: ISO_639_1 = '' ISO_639 = 'ven' ENGLISH_NAME = 'Venda' class VIE: ISO_639_1 = '' ISO_639 = 'vie' ENGLISH_NAME = 'Vietnamese' class VOL: ISO_639_1 = '' ISO_639 = 'vol' ENGLISH_NAME = 'Volapük' class VOT: ISO_639_1 = '' ISO_639 = 'vot' ENGLISH_NAME = 'Votic' class WAL: ISO_639_1 = '' ISO_639 = 'wal' ENGLISH_NAME = 'Wolaitta' class WAR: ISO_639_1 = '' ISO_639 = 'war' ENGLISH_NAME = 'Waray' class WAS: ISO_639_1 = '' ISO_639 = 'was' ENGLISH_NAME = 'Washo' class WEL: ISO_639_1 = '' ISO_639 = 'wel' ENGLISH_NAME = 'Welsh' class WLN: ISO_639_1 = '' ISO_639 = 'wln' ENGLISH_NAME = 'Walloon' class WOL: ISO_639_1 = '' ISO_639 = 'wol' ENGLISH_NAME = 'Wolof' class XAL: ISO_639_1 = '' ISO_639 = 'xal' ENGLISH_NAME = 'Kalmyk' class XHO: ISO_639_1 = '' ISO_639 = 'xho' ENGLISH_NAME = 'Xhosa' class YAO: ISO_639_1 = '' ISO_639 = 'yao' ENGLISH_NAME = 'Yao' class YAP: ISO_639_1 = '' ISO_639 = 'yap' ENGLISH_NAME = 'Yapese' class YID: ISO_639_1 = '' ISO_639 = 'yid' ENGLISH_NAME = 'Yiddish' class YOR: ISO_639_1 = '' ISO_639 = 'yor' ENGLISH_NAME = 'Yoruba' class ZAP: ISO_639_1 = '' ISO_639 = 'zap' ENGLISH_NAME = 'Zapotec' class ZBL: ISO_639_1 = '' ISO_639 = 'zbl' ENGLISH_NAME = 'Blissymbols' class ZEN: ISO_639_1 = '' ISO_639 = 'zen' ENGLISH_NAME = 'Zenaga' class ZGH: ISO_639_1 = '' ISO_639 = 'zgh' ENGLISH_NAME = 'StandardMoroccanTamazight' class ZHA: ISO_639_1 = '' ISO_639 = 'zha' ENGLISH_NAME = 'Zhuang' class ZHS: ISO_639_1 = '' ISO_639 = 'zhs' ENGLISH_NAME = 'SimplifiedChinese' class ZHT: ISO_639_1 = '' ISO_639 = 'zht' ENGLISH_NAME = 'TraditionalChinese' class ZUL: ISO_639_1 = '' ISO_639 = 'zul' ENGLISH_NAME = 'Zulu' class ZUN: ISO_639_1 = '' ISO_639 = 'zun' ENGLISH_NAME = 'Zuni' class ZZA: ISO_639_1 = '' ISO_639 = 'zza' ENGLISH_NAME = 'Zaza' def get_language_classes(): return inspect.getmembers(sys.modules[__name__], inspect.isclass) ================================================ FILE: chatterbot/logic/__init__.py ================================================ from chatterbot.logic.logic_adapter import LogicAdapter from chatterbot.logic.best_match import BestMatch from chatterbot.logic.mathematical_evaluation import MathematicalEvaluation from chatterbot.logic.specific_response import SpecificResponseAdapter from chatterbot.logic.time_adapter import TimeLogicAdapter from chatterbot.logic.unit_conversion import UnitConversion from chatterbot.logic.llm_adapters import ( LLMLogicAdapter, OllamaLogicAdapter, OpenAILogicAdapter, ) __all__ = ( 'LogicAdapter', 'BestMatch', 'MathematicalEvaluation', 'SpecificResponseAdapter', 'TimeLogicAdapter', 'UnitConversion', 'LLMLogicAdapter', 'OllamaLogicAdapter', 'OpenAILogicAdapter', ) ================================================ FILE: chatterbot/logic/best_match.py ================================================ from chatterbot.logic import LogicAdapter from chatterbot.conversation import Statement from chatterbot import filters class BestMatch(LogicAdapter): """ A logic adapter that returns a response based on known responses to the closest matches to the input statement. :param excluded_words: The excluded_words parameter allows a list of words to be set that will prevent the logic adapter from returning statements that have text containing any of those words. This can be useful for preventing your chat bot from saying swears when it is being demonstrated in front of an audience. Defaults to None :type excluded_words: list """ def __init__(self, chatbot, **kwargs): super().__init__(chatbot, **kwargs) self.excluded_words = kwargs.get('excluded_words') def process(self, input_statement: Statement, additional_response_selection_parameters=None) -> Statement: # Get all statements that have a response text similar to the input statement search_results = self.search_algorithm.search(input_statement) # Use the input statement as the closest match if no other results are found input_statement.confidence = 0 # Use 0 confidence when no other results are found closest_match = input_statement # Search for the closest match to the input statement for result in search_results: closest_match = result # Stop searching if a match that is close enough is found if result.confidence >= self.maximum_similarity_threshold: break self.chatbot.logger.info('Selecting "{}" as a response to "{}" with a confidence of {}'.format( closest_match.text, input_statement.text, closest_match.confidence )) # Semantic vector search vs indexed text search have different architectures: # # For SQL with indexed text search: # - Phase 1 finds a match based on string similarity (Levenshtein distance) # - Phase 2 finds variations of that match to get diverse responses # - This makes sense because you might have multiple instances of similar statements # learned from different conversations that provide different response options # # For Redis with semantic vectors: # - Phase 1 finds semantically similar responses using vector embeddings # - The semantic similarity already captures the "closeness" we want # - Phase 2 would be redundant - we already have the best semantic match # - The vector search inherently considers the entire semantic space, not just # exact string matches, so additional variation searching is unnecessary # # NOTE: This difference of functionality may need to be modified in the future # if the redis adapter is determined to benefit from a Phase 2 style response # selection. The main symptom that would drive such a change would be low # quality or repetitive responses when using semantic vector search. # # Therefore, semantic vector search returns the Phase 1 result directly. if self.search_algorithm.name == 'semantic_vector_search' and closest_match.confidence > 0: response = closest_match self.chatbot.logger.info('Using semantic search result directly: "{}"'.format(response.text)) else: # For other search algorithms (indexed_text_search, text_search), # we need to find responses to the closest match recent_repeated_responses = filters.get_recent_repeated_responses( self.chatbot, input_statement.conversation ) for index, recent_repeated_response in enumerate(recent_repeated_responses): self.chatbot.logger.info('{}. Excluding recent repeated response of "{}"'.format( index, recent_repeated_response )) response_selection_parameters = { 'search_text': closest_match.search_text, 'persona_not_startswith': 'bot:', 'exclude_text': recent_repeated_responses, 'exclude_text_words': self.excluded_words } alternate_response_selection_parameters = { 'search_in_response_to': input_statement.search_text or self.chatbot.tagger.get_text_index_string( input_statement.text ), 'persona_not_startswith': 'bot:', 'exclude_text': recent_repeated_responses, 'exclude_text_words': self.excluded_words } if additional_response_selection_parameters: response_selection_parameters.update( additional_response_selection_parameters ) alternate_response_selection_parameters.update( additional_response_selection_parameters ) # Get all statements with text similar to the closest match response_list = list(self.chatbot.storage.filter(**response_selection_parameters)) if response_list: response = self.select_response( input_statement, response_list, self.chatbot.storage ) response.confidence = closest_match.confidence self.chatbot.logger.info('Selecting "{}" from {} optimal responses.'.format( response.text, len(response_list) )) else: ''' The case where there was no responses returned for the selected match but a value exists for the statement the match is in response to. ''' self.chatbot.logger.info('No responses found. Generating alternate response list.') alternate_response_list = list(self.chatbot.storage.filter( **alternate_response_selection_parameters )) if alternate_response_list: response = self.select_response( input_statement, alternate_response_list, self.chatbot.storage ) response.confidence = closest_match.confidence self.chatbot.logger.info('Selected alternative response "{}" from {} options'.format( response.text, len(alternate_response_list) )) else: response = self.get_default_response(input_statement) self.chatbot.logger.info('Using "%s" as a default response.', response.text) return response ================================================ FILE: chatterbot/logic/llm_adapters.py ================================================ """ LLM Logic Adapters for ChatterBot. This module provides logic adapters that integrate Large Language Models. LLM adapters can use other logic adapters as tools via MCP (Model Context Protocol). """ import json from typing import Any, Dict, List, Optional, Union from chatterbot.logic.logic_adapter import LogicAdapter from chatterbot.conversation import Statement from chatterbot.logic.mcp_tools import ( is_tool_adapter, convert_to_openai_tool_format, convert_to_ollama_tool_format ) from chatterbot import utils class LLMLogicAdapter(LogicAdapter): """ Base class for Large Language Model logic adapters. .. warning:: LLM logic adapters are experimental and may change in future releases. Tool calling functionality is still being refined and may have limitations. LLM adapters can participate in ChatterBot's consensus voting mechanism alongside traditional logic adapters. They can also use other logic adapters as tools through MCP. Configuration parameters: model (str): The LLM model name (required) host (str): API endpoint URL (optional, provider-specific default) logic_adapters_as_tools (list): List of logic adapters to expose as tools force_native_tools (bool): Force native tool calling (None=auto-detect) min_confidence (float): Minimum confidence for LLM responses (default: 0.5) max_confidence (float): Maximum confidence for LLM responses (default: 0.85) conversation_context_count (int): Number of previous statements to include (default: 5) system_message (str): Custom system message for the LLM Example: { 'import_path': 'chatterbot.logic.OllamaLogicAdapter', 'model': 'llama3.1', 'logic_adapters_as_tools': [ 'chatterbot.logic.MathematicalEvaluation', 'chatterbot.logic.TimeLogicAdapter' ], 'min_confidence': 0.6, 'max_confidence': 0.9 } """ def __init__(self, chatbot, **kwargs): super().__init__(chatbot, **kwargs) # Model configuration self.model = kwargs.get('model') if not self.model: raise ValueError("LLM logic adapters require a 'model' parameter") self.host = kwargs.get('host') # Confidence range for LLM responses (for consensus voting) self.min_confidence = kwargs.get('min_confidence', 0.5) self.max_confidence = kwargs.get('max_confidence', 0.85) # Conversation context self.conversation_context_count = kwargs.get('conversation_context_count', 5) # System message default_system_message = ( "You are a helpful AI assistant engaged in a direct conversation. " "Address the person you're speaking with directly rather than referring to them in third person. " "Please keep responses concise, conversational, and under 1100 tokens." ) # If tools are configured, enhance system message to clarify tool usage if kwargs.get('logic_adapters_as_tools'): default_system_message += ( "\n\nYou have access to specialized tools that can help you answer certain types of questions. " "Use these tools when they would be helpful, but you should respond naturally to ALL questions, " "not just tool-related ones. For general conversation, greetings, or topics outside the tools' scope, " "respond directly without using tools." ) self.system_message = kwargs.get('system_message', default_system_message) # Tool calling configuration self.force_native_tools = kwargs.get('force_native_tools', None) self.tool_registry = {} self._native_tools_supported = None # Cached tool capability detection result # Initialize tool adapters if provided logic_adapters_as_tools = kwargs.get('logic_adapters_as_tools', []) if logic_adapters_as_tools: self._initialize_tool_adapters(logic_adapters_as_tools, **kwargs) # Detect tool capability once during initialization self._native_tools_supported = self._detect_tool_capability() def _initialize_tool_adapters(self, adapter_configs: List[Union[str, Dict]], **kwargs): """ Initialize logic adapters to be used as tools. Args: adapter_configs: List of adapter import paths or config dicts **kwargs: Additional kwargs to pass to adapters """ for adapter_config in adapter_configs: # Validate and initialize the adapter utils.validate_adapter_class(adapter_config, LogicAdapter) adapter = utils.initialize_class(adapter_config, self.chatbot, **kwargs) # Check if adapter supports tool functionality if is_tool_adapter(adapter): tool_name = adapter.get_tool_name() self.tool_registry[tool_name] = adapter self.chatbot.logger.info( f"Registered tool: {tool_name} from {adapter.__class__.__name__}" ) else: self.chatbot.logger.warning( f"Adapter {adapter.__class__.__name__} does not implement MCPToolAdapter, skipping" ) def _get_conversation_context(self, input_statement: Statement) -> List[Dict[str, str]]: """ Retrieve previous conversation context from storage. .. note:: Security Note: Conversation history is loaded from storage without modification. If you need to scan historical messages for security issues (e.g., context poisoning), override this method in a base class. Args: input_statement: The current input statement Returns: List of message dicts in LLM format """ messages = [] if not input_statement.conversation: return messages try: # Query storage for recent statements in this conversation previous_statements = self.chatbot.storage.filter( conversation=input_statement.conversation, order_by=['id'], page_size=self.conversation_context_count * 2 # x2 to account for bot responses ) # Convert to LLM message format for stmt in previous_statements: # Determine role based on persona if stmt.persona and stmt.persona.startswith('bot:'): role = 'assistant' else: role = 'user' messages.append({ 'role': role, 'content': stmt.text }) except Exception as e: self.chatbot.logger.warning(f"Failed to retrieve conversation context: {e}") return messages def _build_base_messages(self, input_statement: Statement, system_message: Optional[str] = None) -> List[Dict[str, str]]: """ Build base message list for LLM API calls. Args: input_statement: The input statement system_message: Optional system message override Returns: List of message dicts in LLM format """ messages = [{'role': 'system', 'content': system_message or self.system_message}] messages.extend(self._get_conversation_context(input_statement)) messages.append({'role': 'user', 'content': input_statement.text}) return messages def _format_error_response(self, error: Exception) -> str: """ Format a consistent error response message. Args: error: The exception that occurred Returns: Formatted error message string """ return f"I apologize, but I encountered an error: {str(error)}" def _supports_native_tools(self) -> bool: """ Determine if the current model supports native tool calling. Returns: True if native tools are supported """ # If user explicitly set force_native_tools, use that if self.force_native_tools is not None: return self.force_native_tools # Otherwise, use cached detection result # (detection happens once during initialization) if self._native_tools_supported is None: # Fallback: detect now if somehow not set during init self._native_tools_supported = self._detect_tool_capability() return self._native_tools_supported def _detect_tool_capability(self) -> bool: """ Detect if the model supports native tool calling. Override in subclasses for provider-specific detection. Returns: True if tools are supported """ return False def _get_tools_for_llm(self) -> List[Dict[str, Any]]: """ Get tool definitions in the format expected by the LLM provider. Override in subclasses for provider-specific formats. Returns: List of tool definitions """ raise NotImplementedError("Subclasses must implement _get_tools_for_llm()") def _execute_tool(self, tool_name: str, parameters: Dict[str, Any]) -> str: """ Execute a tool by its name with the given parameters. Args: tool_name: Name of the tool to execute parameters: Tool parameters Returns: Tool execution result as string """ if tool_name not in self.tool_registry: self.chatbot.logger.warning(f"Tool not found: '{tool_name}'") return f"Error: Tool '{tool_name}' not found" adapter = self.tool_registry[tool_name] try: # Validate parameters if not adapter.validate_tool_parameters(**parameters): self.chatbot.logger.warning(f"Invalid parameters for tool '{tool_name}': {parameters}") return f"Error: Invalid parameters for tool '{tool_name}'" # Log tool execution self.chatbot.logger.info(f"Executing tool: '{tool_name}' with parameters: {parameters}") # Execute tool result = adapter.execute_as_tool(**parameters) # Convert result to string if needed if not isinstance(result, str): result = str(result) self.chatbot.logger.info(f"Tool '{tool_name}' completed successfully") return result except Exception as e: self.chatbot.logger.error(f"Tool execution error for '{tool_name}': {e}") return f"Error executing tool '{tool_name}': {str(e)}" def _handle_native_tool_calling(self, input_statement: Statement) -> Statement: """ Handle tool calling with native LLM support. Override in subclasses for provider-specific implementation. Args: input_statement: The input statement to process Returns: Response statement with confidence """ raise NotImplementedError("Subclasses must implement _handle_native_tool_calling()") def _handle_prompt_based_tool_calling(self, input_statement: Statement) -> Statement: """ Handle tool calling via prompt engineering for models without native support. This method guides the LLM to output structured JSON that can be parsed and routed to appropriate tools. Args: input_statement: The input statement to process Returns: Response statement with confidence """ # Build tool descriptions for prompt tool_descriptions = [] for adapter in self.tool_registry.values(): schema = adapter.get_tool_schema() tool_desc = f"- {schema['name']}: {schema['description']}" tool_descriptions.append(tool_desc) tools_text = "\n".join(tool_descriptions) # TODO: Consider switching from JSON to TOON # Enhanced system message with tool instructions system_msg = f"""{self.system_message} You have access to the following specialized tools: {tools_text} IMPORTANT: 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. When you need to use a tool, respond with a JSON object in this exact format: {{"tool": "tool_name", "parameters": {{"param1": "value1"}}}} For all other questions, respond normally with plain text conversationally.""" # Get LLM response response_text = self._call_llm(input_statement, system_msg) # Try to parse as JSON (tool call) if response_text.strip().startswith('{'): try: tool_call = json.loads(response_text) tool_name = tool_call.get('tool') parameters = tool_call.get('parameters', {}) self.chatbot.logger.info(f"LLM requested tool via prompt: '{tool_name}'") # Execute tool tool_result = self._execute_tool(tool_name, parameters) # Get final response from LLM with tool result followup_msg = f"Tool '{tool_name}' returned: {tool_result}\nProvide a natural language response to the user." final_response = self._call_llm_with_context(input_statement, followup_msg) response = Statement(text=final_response) response.confidence = self._calculate_confidence(final_response) return response except json.JSONDecodeError: pass # Not a tool call, treat as normal response # Regular text response response = Statement(text=response_text) response.confidence = self._calculate_confidence(response_text) return response def _call_llm(self, input_statement: Statement, system_message: Optional[str] = None) -> str: """ Make a direct LLM API call without tool support. Override in subclasses for provider-specific implementation. Args: input_statement: The input statement system_message: Optional system message override Returns: LLM response text """ raise NotImplementedError("Subclasses must implement _call_llm()") def _call_llm_with_context(self, input_statement: Statement, additional_context: str) -> str: """ Make an LLM call with additional context message. Args: input_statement: The input statement additional_context: Additional context to include Returns: LLM response text """ # This will be implemented in subclasses using their specific API raise NotImplementedError("Subclasses must implement _call_llm_with_context()") def _calculate_confidence(self, response_text: str) -> float: """ Calculate confidence score for LLM response. Uses a simple heuristic based on response length and quality indicators. Returns a value between min_confidence and max_confidence. Args: response_text: The LLM's response text Returns: Confidence score between 0 and 1 """ # Base confidence (middle of range) confidence = (self.min_confidence + self.max_confidence) / 2 # Adjust based on response length (very short or very long may be less reliable) length = len(response_text) if length < 10: confidence -= 0.1 elif 50 < length < 200: confidence += 0.05 # Clamp to configured range confidence = max(self.min_confidence, min(self.max_confidence, confidence)) return confidence def process(self, statement: Statement, additional_response_selection_parameters: dict = None) -> Statement: """ Process the input statement using the LLM. Args: statement: The input statement to process additional_response_selection_parameters: Additional parameters (unused) Returns: Response statement with confidence score """ # If no tools are configured, just call LLM directly if not self.tool_registry: response_text = self._call_llm(statement) response = Statement(text=response_text) response.confidence = self._calculate_confidence(response_text) return response # Determine tool calling method if self._supports_native_tools(): return self._handle_native_tool_calling(statement) else: return self._handle_prompt_based_tool_calling(statement) class OllamaLogicAdapter(LLMLogicAdapter): """ Logic adapter for Ollama LLMs with MCP tool support. .. warning:: This adapter is experimental. Tool capability detection uses template inspection which may not work for all model formats. Tool calling behavior varies significantly between models. Configuration: model (str): Ollama model name (e.g., 'llama3.1', 'mistral') host (str): Ollama API endpoint (default: http://localhost:11434) logic_adapters_as_tools (list): Logic adapters to expose as tools Example: { 'import_path': 'chatterbot.logic.OllamaLogicAdapter', 'model': 'llama3.1', 'host': 'http://localhost:11434', 'logic_adapters_as_tools': [ 'chatterbot.logic.MathematicalEvaluation', 'chatterbot.logic.TimeLogicAdapter' ] } """ def __init__(self, chatbot, **kwargs): # Set default host before parent init if 'host' not in kwargs: kwargs['host'] = 'http://localhost:11434' super().__init__(chatbot, **kwargs) # Initialize Ollama client try: from ollama import Client self.client = Client(host=self.host) except ImportError: raise ImportError( "Ollama library not installed. Install with: pip install chatterbot[dev]" ) def _detect_tool_capability(self) -> bool: """ Detect if the Ollama model supports native tool calling. Uses a combination of known model patterns and template inspection to determine tool support. Returns: True if model supports tools """ # Known models with tool support (as of 2026) # Check model name patterns - handles versioned models (e.g., llama3.1:8b) model_base = self.model.split(':')[0].lower() # Known tool-supporting model patterns tool_supporting_patterns = [ # Llama series 'llama3.1', 'llama3.2', 'llama3-groq-tool', # Mistral series 'mistral', 'mistral-nemo', 'mistral-large', # Qwen series 'qwen2.5', 'qwen2.5-coder', # Specialized models 'firefunction', 'nemotron', 'command-r', 'command-r-plus', # Enterprise models 'granite3.1-dense', 'hermes3' ] # Check if model matches any known pattern for pattern in tool_supporting_patterns: if pattern in model_base: self.chatbot.logger.info( f"Model '{self.model}' supports native tool calling (known model)" ) return True # Fallback to template inspection for unknown models try: # Get model metadata model_info = self.client.show(self.model) # Get the template string template = model_info.get('template', '') # Check for tool-specific tokens in the template has_tools = '{{ .Tools }}' in template or '{{ tools }}' in template if has_tools: self.chatbot.logger.info( f"Model '{self.model}' supports native tool calling (template inspection)" ) else: self.chatbot.logger.info( f"Model '{self.model}' does not support native tool calling, will use prompt-based approach" ) return has_tools except Exception as e: self.chatbot.logger.warning( f"Failed to inspect model '{self.model}' for tool support: {e}. " f"Falling back to prompt-based tool calling." ) return False def _get_tools_for_llm(self) -> List[Dict[str, Any]]: """ Get tool definitions in Ollama format. Returns: List of Ollama-formatted tool definitions """ tools = [] for adapter in self.tool_registry.values(): schema = adapter.get_tool_schema() ollama_tool = convert_to_ollama_tool_format(schema) tools.append(ollama_tool) return tools def _call_llm(self, input_statement: Statement, system_message: Optional[str] = None) -> str: """ Call Ollama API without tool support. Args: input_statement: The input statement system_message: Optional system message override Returns: LLM response text """ # Build messages with conversation context messages = self._build_base_messages(input_statement, system_message) try: response = self.client.chat( model=self.model, messages=messages ) return response['message']['content'] except Exception as e: self.chatbot.logger.error(f"Ollama API error: {e}") return self._format_error_response(e) def _call_llm_with_context(self, input_statement: Statement, additional_context: str) -> str: """ Call Ollama with additional context for tool result processing. Args: input_statement: The input statement additional_context: Additional context message Returns: LLM response text """ messages = self._build_base_messages(input_statement) messages.append({'role': 'assistant', 'content': additional_context}) try: response = self.client.chat( model=self.model, messages=messages ) return response['message']['content'] except Exception as e: self.chatbot.logger.error(f"Ollama API error: {e}") return self._format_error_response(e) def _handle_native_tool_calling(self, input_statement: Statement) -> Statement: """ Handle tool calling with Ollama's native function calling support. Args: input_statement: The input statement to process Returns: Response statement with confidence """ # Build messages messages = self._build_base_messages(input_statement) # Get tools in Ollama format tools = self._get_tools_for_llm() # TODO: Look into support for thinking mode try: # Initial LLM call with tools response = self.client.chat( model=self.model, messages=messages, tools=tools ) message = response['message'] # Check if LLM wants to use a tool if tool_calls := message.get('tool_calls'): self.chatbot.logger.info(f"Ollama LLM requested {len(tool_calls)} tool(s)") # Serialize the message properly for Ollama API # The message object needs to be converted to dict format if hasattr(message, 'model_dump'): # Pydantic v2 message_dict = message.model_dump(exclude_none=True) elif hasattr(message, 'dict'): # Pydantic v1 message_dict = message.dict(exclude_none=True) else: # Fallback if it's already a dict or needs manual conversion message_dict = dict(message) if not isinstance(message, dict) else message messages.append(message_dict) # Execute each tool call and add results for tool_call in tool_calls: function = tool_call['function'] tool_name = function['name'] parameters = function.get('arguments', {}) # Execute tool tool_result = self._execute_tool(tool_name, parameters) # Add tool result to conversation with tool_name field messages.append({ 'role': 'tool', 'content': tool_result, 'tool_name': tool_name }) # Get final response from LLM with tool results final_response = self.client.chat( model=self.model, messages=messages, tools=tools ) response_text = final_response['message']['content'] else: # No tool call, use direct response response_text = message['content'] response = Statement(text=response_text) response.confidence = self._calculate_confidence(response_text) return response except Exception as e: self.chatbot.logger.error(f"Ollama tool calling error: {e}") response = Statement(text=self._format_error_response(e)) response.confidence = self.min_confidence return response class OpenAILogicAdapter(LLMLogicAdapter): """ Logic adapter for OpenAI LLMs with MCP tool support. .. warning:: This adapter is experimental. Configuration: model (str): OpenAI model name (e.g., 'gpt-4', 'gpt-3.5-turbo') host (str): Optional custom API endpoint logic_adapters_as_tools (list): Logic adapters to expose as tools Environment: OPENAI_API_KEY: Required for authentication Example: { 'import_path': 'chatterbot.logic.OpenAILogicAdapter', 'model': 'gpt-4o-mini', 'logic_adapters_as_tools': [ 'chatterbot.logic.MathematicalEvaluation', 'chatterbot.logic.TimeLogicAdapter' ] } """ def __init__(self, chatbot, **kwargs): super().__init__(chatbot, **kwargs) # Initialize OpenAI client try: from openai import OpenAI as OpenAIClient if self.host: self.client = OpenAIClient(base_url=self.host) else: self.client = OpenAIClient() except ImportError: raise ImportError( "OpenAI library not installed. Install with: pip install chatterbot[dev]" ) def _detect_tool_capability(self) -> bool: """ Detect if the OpenAI model supports tool calling. Returns: True (all current OpenAI models support tool calling) """ return True def _get_tools_for_llm(self) -> List[Dict[str, Any]]: """ Get tool definitions in OpenAI format. Returns: List of OpenAI-formatted tool definitions """ tools = [] for adapter in self.tool_registry.values(): schema = adapter.get_tool_schema() openai_tool = convert_to_openai_tool_format(schema) tools.append(openai_tool) return tools def _call_llm(self, input_statement: Statement, system_message: Optional[str] = None) -> str: """ Call OpenAI API without tool support. Args: input_statement: The input statement system_message: Optional system message override Returns: LLM response text """ # Build messages with conversation context messages = self._build_base_messages(input_statement, system_message) try: response = self.client.chat.completions.create( model=self.model, messages=messages ) return response.choices[0].message.content except Exception as e: self.chatbot.logger.error(f"OpenAI API error: {e}") return self._format_error_response(e) def _call_llm_with_context(self, input_statement: Statement, additional_context: str) -> str: """ Call OpenAI with additional context for tool result processing. Args: input_statement: The input statement additional_context: Additional context message Returns: LLM response text """ messages = self._build_base_messages(input_statement) messages.append({'role': 'assistant', 'content': additional_context}) try: response = self.client.chat.completions.create( model=self.model, messages=messages ) return response.choices[0].message.content except Exception as e: self.chatbot.logger.error(f"OpenAI API error: {e}") return self._format_error_response(e) def _handle_native_tool_calling(self, input_statement: Statement) -> Statement: """ Handle tool calling with OpenAI's native function calling support. Args: input_statement: The input statement to process Returns: Response statement with confidence """ # Build messages messages = self._build_base_messages(input_statement) # Get tools in OpenAI format tools = self._get_tools_for_llm() try: # Initial LLM call with tools response = self.client.chat.completions.create( model=self.model, messages=messages, tools=tools ) message = response.choices[0].message # Check if LLM wants to use a tool if tool_calls := message.tool_calls: self.chatbot.logger.info(f"OpenAI LLM requested {len(tool_calls)} tool(s)") # Execute each tool call for tool_call in tool_calls: function = tool_call.function tool_name = function.name parameters = json.loads(function.arguments) # Execute tool tool_result = self._execute_tool(tool_name, parameters) # Add assistant message with tool call messages.append({ 'role': 'assistant', 'content': None, 'tool_calls': [{ 'id': tool_call.id, 'type': 'function', 'function': { 'name': tool_name, 'arguments': function.arguments } }] }) # Add tool result message messages.append({ 'role': 'tool', 'tool_call_id': tool_call.id, 'content': tool_result }) # Get final response from LLM with tool results final_response = self.client.chat.completions.create( model=self.model, messages=messages, tools=tools ) response_text = final_response.choices[0].message.content else: # No tool call, use direct response response_text = message.content response = Statement(text=response_text) response.confidence = self._calculate_confidence(response_text) return response except Exception as e: self.chatbot.logger.error(f"OpenAI tool calling error: {e}") response = Statement(text=self._format_error_response(e)) response.confidence = self.min_confidence return response ================================================ FILE: chatterbot/logic/logic_adapter.py ================================================ from random import choice from chatterbot.adapters import Adapter from chatterbot.storage import StorageAdapter from chatterbot.search import IndexedTextSearch from chatterbot.conversation import Statement from chatterbot import utils class LogicAdapter(Adapter): """ This is an abstract class that represents the interface that all logic adapters should implement. :param search_algorithm_name: The name of the search algorithm that should be used to search for close matches to the provided input. Defaults to the value of ``Search.name``. :param maximum_similarity_threshold: The maximum amount of similarity between two statement that is required before the search process is halted. The search for a matching statement will continue until a statement with a greater than or equal similarity is found or the search set is exhausted. Defaults to 0.95 :param response_selection_method: The a response selection method. Defaults to ``get_first_response`` :type response_selection_method: collections.abc.Callable :param default_response: The default response returned by this logic adapter if there is no other possible response to return. :type default_response: str or list or tuple """ def __init__(self, chatbot, **kwargs): super().__init__(chatbot, **kwargs) from chatterbot.response_selection import get_first_response self.search_algorithm_name = kwargs.get( 'search_algorithm_name', IndexedTextSearch.name ) self.search_algorithm = self.chatbot.search_algorithms[ self.search_algorithm_name ] self.maximum_similarity_threshold = kwargs.get( 'maximum_similarity_threshold', 0.95 ) if response_selection_method := kwargs.get('response_selection_method'): if isinstance(response_selection_method, str): # If an import path is provided, import the method response_selection_method = utils.import_module( response_selection_method ) kwargs['response_selection_method'] = response_selection_method # By default, select the first available response self.select_response = kwargs.get( 'response_selection_method', get_first_response ) default_responses = kwargs.get('default_response', []) # Convert a single string into a list if isinstance(default_responses, str): default_responses = [ default_responses ] self.default_responses = [ Statement(text=default) for default in default_responses ] def can_process(self, statement) -> bool: """ A preliminary check that is called to determine if a logic adapter can process a given statement. By default, this method returns true but it can be overridden in child classes as needed. """ return True def process(self, statement: Statement, additional_response_selection_parameters: dict = None) -> Statement: """ Override this method and implement your logic for selecting a response to an input statement. A confidence value and the selected response statement should be returned. The confidence value represents a rating of how accurate the logic adapter expects the selected response to be. Confidence scores are used to select the best response from multiple logic adapters. The confidence value should be a number between 0 and 1 where 0 is the lowest confidence level and 1 is the highest. :param statement: An input statement to be processed by the logic adapter. :param additional_response_selection_parameters: Parameters to be used when filtering results to choose a response from. """ raise self.AdapterMethodNotImplementedError() def get_default_response(self, input_statement: Statement) -> Statement: """ This method is called when a logic adapter is unable to generate any other meaningful response. """ if self.default_responses: response = choice(self.default_responses) else: try: response = self.chatbot.storage.get_random() except StorageAdapter.EmptyDatabaseException: response = input_statement self.chatbot.logger.info( 'No known response to the input was found. Selecting a random response.' ) # Set confidence to zero because a random response is selected response.confidence = 0 return response @property def class_name(self) -> str: """ Return the name of the current logic adapter class. This is typically used for logging and debugging. """ return str(self.__class__.__name__) ================================================ FILE: chatterbot/logic/mathematical_evaluation.py ================================================ from chatterbot.logic import LogicAdapter from chatterbot.conversation import Statement from chatterbot import languages from chatterbot.logic.mcp_tools import MCPToolAdapter class MathematicalEvaluation(LogicAdapter, MCPToolAdapter): """ The MathematicalEvaluation logic adapter parses input to determine whether the user is asking a question that requires math to be done. If so, the equation is extracted from the input and returned with the evaluated result. For example: User: 'What is three plus five?' Bot: 'Three plus five equals eight' :kwargs: * *language* (``object``) -- The language is set to ``chatterbot.languages.ENG`` for English by default. """ def __init__(self, chatbot, **kwargs): super().__init__(chatbot, **kwargs) self.language = kwargs.get('language', languages.ENG) self.cache = {} def can_process(self, statement) -> bool: """ Determines whether it is appropriate for this adapter to respond to the user input. """ response = self.process(statement) self.cache[statement.text] = response return response.confidence == 1 def process(self, statement: Statement, additional_response_selection_parameters: dict = None) -> Statement: """ Takes a statement string. Returns the equation from the statement with the mathematical terms solved. """ from mathparse import mathparse input_text = statement.text # Use the result cached by the process method if it exists if input_text in self.cache: cached_result = self.cache[input_text] self.cache = {} return cached_result # Getting the mathematical terms within the input statement expression = mathparse.extract_expression(input_text, language=self.language.ISO_639.upper()) response = Statement(text=expression) try: response.text = '{} = {}'.format( response.text, mathparse.parse(expression, language=self.language.ISO_639.upper()) ) # The confidence is 1 if the expression could be evaluated response.confidence = 1 except mathparse.PostfixTokenEvaluationException: response.confidence = 0 return response def get_tool_schema(self): """ Return the MCP tool schema for mathematical evaluation. """ return { "name": "calculate", "description": "Evaluate mathematical expressions and solve equations. Supports basic arithmetic, algebra, and common mathematical functions.", "parameters": { "type": "object", "properties": { "expression": { "type": "string", "description": "The mathematical expression to evaluate (e.g., '2 + 2', 'sqrt(16)', 'three plus five')" } }, "required": ["expression"] } } def execute_as_tool(self, **kwargs): """ Execute mathematical evaluation as a tool. Args: **kwargs: Must contain 'expression' parameter Returns: The evaluation result as a string """ from mathparse import mathparse expression = kwargs.get("expression", "") if not expression: return "Error: No expression provided" try: # Extract mathematical expression extracted = mathparse.extract_expression(expression, language=self.language.ISO_639.upper()) # Evaluate the expression result = mathparse.parse(extracted, language=self.language.ISO_639.upper()) return f"{extracted} = {result}" except mathparse.PostfixTokenEvaluationException: return f"Error: Could not evaluate expression '{expression}'" except Exception as e: return f"Error: {str(e)}" ================================================ FILE: chatterbot/logic/mcp_tools.py ================================================ """ MCP (Model Context Protocol) tool adapter for ChatterBot logic adapters. This module provides a mixin class that allows logic adapters to be exposed as MCP-compatible tools to LLMs. Logic adapters that inherit from MCPToolAdapter can define tool schemas and be invoked by LLM adapters. """ from typing import Any, Dict from abc import ABC, abstractmethod class MCPToolAdapter(ABC): """ Mixin class for logic adapters that can be used as MCP tools. Logic adapters that want to be callable as tools should inherit from this class and implement the get_tool_schema() and execute_as_tool() methods. Example: class MathematicalEvaluation(LogicAdapter, MCPToolAdapter): def get_tool_schema(self) -> Dict[str, Any]: return { "name": "calculate", "description": "Evaluate mathematical expressions", "parameters": { "type": "object", "properties": { "expression": { "type": "string", "description": "Mathematical expression to evaluate" } }, "required": ["expression"] } } def execute_as_tool(self, **kwargs) -> str: expression = kwargs.get("expression") # ... evaluation logic return result """ @abstractmethod def get_tool_schema(self) -> Dict[str, Any]: """ Return the tool schema for this logic adapter. The schema should follow the OpenAI/MCP function calling format: { "name": "tool_name", "description": "Tool description", "parameters": { "type": "object", "properties": { "param_name": { "type": "string|number|boolean|array|object", "description": "Parameter description" } }, "required": ["param_name"] } } Returns: Dict containing the tool schema """ raise NotImplementedError( "Logic adapters using MCPToolAdapter must implement get_tool_schema()" ) @abstractmethod def execute_as_tool(self, **kwargs) -> Any: """ Execute this logic adapter as a tool with the given parameters. This method is called when an LLM requests to use this adapter as a tool. It should extract the necessary parameters from kwargs and execute the logic adapter's functionality in a tool-calling context. Args: **kwargs: Tool parameters as defined in the tool schema Returns: Tool execution result (will be converted to string if needed) """ raise NotImplementedError( "Logic adapters using MCPToolAdapter must implement execute_as_tool()" ) def get_tool_name(self) -> str: """ Get the name of this tool. Returns: The tool name from the schema """ schema = self.get_tool_schema() return schema.get("name", self.__class__.__name__) def validate_tool_parameters(self, **kwargs) -> bool: """ Validate that the provided parameters match the tool schema. Args: **kwargs: Parameters to validate Returns: True if parameters are valid, False otherwise """ schema = self.get_tool_schema() parameters = schema.get("parameters", {}) required = parameters.get("required", []) properties = parameters.get("properties", {}) # Check required parameters for param in required: if param not in kwargs: return False # Check parameter types (basic validation) for param_name, param_value in kwargs.items(): if param_name not in properties: continue expected_type = properties[param_name].get("type") if expected_type == "string" and not isinstance(param_value, str): return False elif expected_type == "number" and not isinstance(param_value, (int, float)): return False elif expected_type == "boolean" and not isinstance(param_value, bool): return False elif expected_type == "array" and not isinstance(param_value, list): return False elif expected_type == "object" and not isinstance(param_value, dict): return False return True def is_tool_adapter(adapter) -> bool: """ Check if a logic adapter instance supports MCP tool functionality. Args: adapter: Logic adapter instance to check Returns: True if the adapter has MCPToolAdapter capabilities """ return ( hasattr(adapter, 'get_tool_schema') and callable(adapter.get_tool_schema) and hasattr(adapter, 'execute_as_tool') and callable(adapter.execute_as_tool) ) def convert_to_openai_tool_format(schema: Dict[str, Any]) -> Dict[str, Any]: """ Convert MCP tool schema to OpenAI function calling format. OpenAI expects: { "type": "function", "function": { "name": "...", "description": "...", "parameters": {...} } } Args: schema: MCP tool schema Returns: OpenAI-formatted tool definition """ return { "type": "function", "function": { "name": schema.get("name"), "description": schema.get("description", ""), "parameters": schema.get("parameters", {}) } } def convert_to_ollama_tool_format(schema: Dict[str, Any]) -> Dict[str, Any]: """ Convert MCP tool schema to Ollama function calling format. Ollama uses a similar format to OpenAI: { "type": "function", "function": { "name": "...", "description": "...", "parameters": {...} } } Args: schema: MCP tool schema Returns: Ollama-formatted tool definition """ # Ollama format is identical to OpenAI for now return convert_to_openai_tool_format(schema) ================================================ FILE: chatterbot/logic/specific_response.py ================================================ from chatterbot.logic import LogicAdapter from chatterbot.conversation import Statement from chatterbot import languages from chatterbot.utils import get_model_for_language import spacy class SpecificResponseAdapter(LogicAdapter): """ Return a specific response to a specific input. :kwargs: * *input_text* (``str``) -- The input text that triggers this logic adapter. * *output_text* (``str`` or ``function``) -- The output text returned by this logic adapter. If a function is provided, it should return a string. """ def __init__(self, chatbot, **kwargs): super().__init__(chatbot, **kwargs) try: self.input_text = kwargs['input_text'] except KeyError: raise chatbot.ChatBotException( 'The SpecificResponseAdapter requires an input_text parameter.' ) try: self._output_text = kwargs['output_text'] except KeyError: raise chatbot.ChatBotException( 'The SpecificResponseAdapter requires an output_text parameter.' ) self.matcher = None if MatcherClass := kwargs.get('matcher'): language = kwargs.get('language', languages.ENG) self.nlp = self._initialize_nlp(language) self.matcher = MatcherClass(self.nlp.vocab) self.matcher.add('SpecificResponse', [self.input_text]) def _initialize_nlp(self, language): model = get_model_for_language(language) return spacy.load(model) def can_process(self, statement) -> bool: if self.matcher: doc = self.nlp(statement.text) matches = self.matcher(doc) if matches: return True elif statement.text == self.input_text: return True return False def process(self, statement: Statement, additional_response_selection_parameters: dict = None) -> Statement: if callable(self._output_text): response_statement = Statement(text=self._output_text()) else: response_statement = Statement(text=self._output_text) if self.matcher: doc = self.nlp(statement.text) matches = self.matcher(doc) if matches: response_statement.confidence = 1 else: response_statement.confidence = 0 elif statement.text == self.input_text: response_statement.confidence = 1 else: response_statement.confidence = 0 return response_statement ================================================ FILE: chatterbot/logic/time_adapter.py ================================================ from datetime import datetime from chatterbot import languages from chatterbot.logic import LogicAdapter from chatterbot.conversation import Statement from chatterbot.utils import get_model_for_language from chatterbot.logic.mcp_tools import MCPToolAdapter import spacy class TimeLogicAdapter(LogicAdapter, MCPToolAdapter): """ The TimeLogicAdapter returns the current time. :kwargs: * *positive* (``list``) -- The time-related questions used to identify time questions about the current time. Defaults to a list of English sentences. * *language* (``str``) -- The language for the spacy model. Defaults to English. """ def __init__(self, chatbot, **kwargs): super().__init__(chatbot, **kwargs) # TODO / FUTURE: Switch `positive` to `patterns` for more accurate naming phrases = kwargs.get('positive', [ 'What time is it?', 'Hey, what time is it?', 'Do you have the time?', 'Do you know the time?', 'Do you know what time it is?', 'What is the time?', 'What time is it now?', 'Can you tell me the time?', 'Could you tell me the time?', 'What is the current time?', ]) language = kwargs.get('language', languages.ENG) model = get_model_for_language(language) self.nlp = spacy.load(model) # Set up rules for spacy's rule-based matching # https://spacy.io/usage/rule-based-matching self.matcher = spacy.matcher.PhraseMatcher(self.nlp.vocab) patterns = [self.nlp.make_doc(text) for text in phrases] # Add the patterns to the matcher self.matcher.add('TimeQuestionList', patterns) def process(self, statement: Statement, additional_response_selection_parameters: dict = None) -> Statement: now = datetime.now() # Check if the input statement contains a time-related question doc = self.nlp(statement.text) matches = self.matcher(doc) self.chatbot.logger.info('TimeLogicAdapter detected {} matches'.format(len(matches))) confidence = 1 if matches else 0 response = Statement(text='The current time is ' + now.strftime('%I:%M %p')) response.confidence = confidence return response def get_tool_schema(self): """ Return the MCP tool schema for getting current time. """ return { "name": "get_current_time", "description": "Get the current date and time. Returns formatted time string.", "parameters": { "type": "object", "properties": {}, "required": [] } } def execute_as_tool(self, **kwargs): """ Execute time query as a tool. Returns: Current time as formatted string """ now = datetime.now() return f"The current time is {now.strftime('%I:%M %p')} on {now.strftime('%A, %B %d, %Y')}" ================================================ FILE: chatterbot/logic/unit_conversion.py ================================================ from chatterbot.logic import LogicAdapter from chatterbot.conversation import Statement from chatterbot.exceptions import OptionalDependencyImportError from chatterbot import languages from chatterbot import parsing from chatterbot.logic.mcp_tools import MCPToolAdapter from mathparse import mathparse import re class UnitConversion(LogicAdapter, MCPToolAdapter): """ The UnitConversion logic adapter parse inputs to convert values between several metric units. For example: User: 'How many meters are in one kilometer?' Bot: '1000.0' :kwargs: * *language* (``object``) -- The language is set to ``chatterbot.languages.ENG`` for English by default. """ def __init__(self, chatbot, **kwargs): super().__init__(chatbot, **kwargs) try: from pint import UnitRegistry except ImportError: message = ( 'Unable to import "pint".\n' 'Please install "pint" before using the UnitConversion logic adapter:\n' 'pip install pint' ) raise OptionalDependencyImportError(message) self.language = kwargs.get('language', languages.ENG) self.cache = {} self.patterns = [ ( re.compile(r''' (([Hh]ow\s+many)\s+ (?P\S+)\s+ # meter, celsius, hours ((are)*\s*in)\s+ (?P([+-]?\d+(?:\.\d+)?)|(a|an)|(%s[-\s]?)+)\s+ (?P\S+)\s*) # meter, celsius, hours ''' % (parsing.numbers), (re.VERBOSE | re.IGNORECASE) ), lambda m: self.handle_matches(m) ), ( re.compile(r''' ((?P([+-]?\d+(?:\.\d+)?)|(%s[-\s]?)+)\s+ (?P\S+)\s+ # meter, celsius, hours (to)\s+ (?P\S+)\s*) # meter, celsius, hours ''' % (parsing.numbers), (re.VERBOSE | re.IGNORECASE) ), lambda m: self.handle_matches(m) ), ( re.compile(r''' ((?P([+-]?\d+(?:\.\d+)?)|(a|an)|(%s[-\s]?)+)\s+ (?P\S+)\s+ # meter, celsius, hours (is|are)\s+ (how\s+many)*\s+ (?P\S+)\s*) # meter, celsius, hours ''' % (parsing.numbers), (re.VERBOSE | re.IGNORECASE) ), lambda m: self.handle_matches(m) ) ] self.unit_registry = UnitRegistry() def get_unit(self, unit_variations): """ Get the first match unit metric object supported by pint library given a variation of unit metric names (Ex:['HOUR', 'hour']). :param unit_variations: A list of strings with names of units :type unit_variations: str """ for unit in unit_variations: try: return getattr(self.unit_registry, unit) except AttributeError: continue return None def get_valid_units(self, from_unit, target_unit): """ Returns the first match `pint.unit.Unit` object for from_unit and target_unit strings from a possible variation of metric unit names supported by pint library. :param from_unit: source metric unit :type from_unit: str :param from_unit: target metric unit :type from_unit: str """ from_unit_variations = [from_unit.lower(), from_unit.upper()] target_unit_variations = [target_unit.lower(), target_unit.upper()] from_unit = self.get_unit(from_unit_variations) target_unit = self.get_unit(target_unit_variations) return from_unit, target_unit def handle_matches(self, match): """ Returns a response statement from a matched input statement. :param match: It is a valid matched pattern from the input statement :type: `_sre.SRE_Match` """ response = Statement(text='') from_parsed = match.group("from") target_parsed = match.group("target") n_statement = match.group("number") if n_statement == 'a' or n_statement == 'an': n_statement = '1.0' n = mathparse.parse(n_statement, self.language.ISO_639.upper()) from_parsed, target_parsed = self.get_valid_units(from_parsed, target_parsed) if from_parsed is None or target_parsed is None: response.confidence = 0.0 else: from_value = self.unit_registry.Quantity(float(n), from_parsed) target_value = from_value.to(target_parsed) response.confidence = 1.0 response.text = str(target_value.magnitude) return response def can_process(self, statement) -> bool: response = self.process(statement) self.cache[statement.text] = response return response.confidence == 1.0 def process(self, statement: Statement, additional_response_selection_parameters: dict = None) -> Statement: response = Statement(text='') input_text = statement.text try: # Use the result cached by the process method if it exists if input_text in self.cache: response = self.cache[input_text] self.cache = {} return response for pattern, func in self.patterns: p = pattern.match(input_text) if p is not None: response = func(p) if response.confidence == 1.0: break except Exception as e: self.chatbot.logger.warning('Error during UnitConversion: {}'.format(str(e))) response.confidence = 0.0 return response def get_tool_schema(self): """ Return the MCP tool schema for unit conversion. """ return { "name": "convert_units", "description": "Convert values between different units of measurement. Supports distance, weight, temperature, time, and other common unit conversions.", "parameters": { "type": "object", "properties": { "query": { "type": "string", "description": "Unit conversion query in natural language (e.g., 'How many meters are in 5 kilometers?', '100 fahrenheit to celsius')" } }, "required": ["query"] } } def execute_as_tool(self, **kwargs): """ Execute unit conversion as a tool. Args: **kwargs: Must contain 'query' parameter Returns: The conversion result as a string """ query = kwargs.get("query", "") if not query: return "Error: No conversion query provided" try: # Create a statement and process it input_statement = Statement(text=query) response = self.process(input_statement) if response.confidence == 1.0: return response.text else: 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?'" except Exception as e: return f"Error: {str(e)}" ================================================ FILE: chatterbot/parsing.py ================================================ import re from datetime import timedelta, datetime import calendar # Variations of dates that the parser can capture year_variations = ['year', 'years', 'yrs'] day_variations = ['days', 'day'] minute_variations = ['minute', 'minutes', 'mins'] hour_variations = ['hrs', 'hours', 'hour'] week_variations = ['weeks', 'week', 'wks'] month_variations = ['month', 'months'] # Variables used for RegEx Matching day_names = 'monday|tuesday|wednesday|thursday|friday|saturday|sunday' month_names_long = ( 'january|february|march|april|may|june|july|august|september|october|november|december' ) month_names = month_names_long + '|jan|feb|mar|apr|may|jun|jul|aug|sep|oct|nov|dec' day_nearest_names = 'today|yesterday|tomorrow|tonight|tonite' numbers = ( r'(^a(?=\s)|one|two|three|four|five|six|seven|eight|nine|ten|' r'eleven|twelve|thirteen|fourteen|fifteen|sixteen|seventeen|' r'eighteen|nineteen|twenty|thirty|forty|fifty|sixty|seventy|' r'eighty|ninety|hundred|thousand)' ) re_dmy = '(' + '|'.join(day_variations + minute_variations + year_variations + week_variations + month_variations) + ')' re_duration = r'(before|after|earlier|later|ago|from\snow)' re_year = r'(19|20)\d{2}|^(19|20)\d{2}' re_timeframe = r'this|coming|next|following|previous|last|end\sof\sthe' re_ordinal = r'st|nd|rd|th|first|second|third|fourth|fourth|' + re_timeframe re_time = r'(?P\d{1,2})(?=\s?(\:\d|(a|p)m))(\:(?P\d{1,2}))?(\s?(?P(am|pm)))?' re_separator = r'of|at|on' NUMBERS = { 'zero': 0, 'one': 1, 'two': 2, 'three': 3, 'four': 4, 'five': 5, 'six': 6, 'seven': 7, 'eight': 8, 'nine': 9, 'ten': 10, 'eleven': 11, 'twelve': 12, 'thirteen': 13, 'fourteen': 14, 'fifteen': 15, 'sixteen': 16, 'seventeen': 17, 'eighteen': 18, 'nineteen': 19, 'twenty': 20, 'thirty': 30, 'forty': 40, 'fifty': 50, 'sixty': 60, 'seventy': 70, 'eighty': 80, 'ninety': 90, 'hundred': 100, 'thousand': 1000, 'million': 1000000, 'billion': 1000000000, 'trillion': 1000000000000, } # Mapping of Month name and Value HASHMONTHS = { 'january': 1, 'jan': 1, 'february': 2, 'feb': 2, 'march': 3, 'mar': 3, 'april': 4, 'apr': 4, 'may': 5, 'june': 6, 'jun': 6, 'july': 7, 'jul': 7, 'august': 8, 'aug': 8, 'september': 9, 'sep': 9, 'october': 10, 'oct': 10, 'november': 11, 'nov': 11, 'december': 12, 'dec': 12 } # Days to number mapping HASHWEEKDAYS = { 'monday': 0, 'mon': 0, 'tuesday': 1, 'tue': 1, 'wednesday': 2, 'wed': 2, 'thursday': 3, 'thu': 3, 'friday': 4, 'fri': 4, 'saturday': 5, 'sat': 5, 'sunday': 6, 'sun': 6 } # Ordinal to number HASHORDINALS = { 'zeroth': 0, 'first': 1, 'second': 2, 'third': 3, 'fourth': 4, 'forth': 4, 'fifth': 5, 'sixth': 6, 'seventh': 7, 'eighth': 8, 'ninth': 9, 'tenth': 10, 'eleventh': 11, 'twelfth': 12, 'thirteenth': 13, 'fourteenth': 14, 'fifteenth': 15, 'sixteenth': 16, 'seventeenth': 17, 'eighteenth': 18, 'nineteenth': 19, 'twentieth': 20, 'last': -1 } # A list tuple of regular expressions / parser fn to match # Start with the widest match and narrow it down because the order of the match in this list matters regex = [ ( re.compile( r''' ( ((?P%s)[,\s]\s*)? #Matches Monday, 12 Jan 2012, 12 Jan 2012 etc (?P\d{1,2}) # Matches a digit (%s)? [-\s] # One or more space (?P%s) # Matches any month name [-\s] # Space (?P%s) # Year ((\s|,\s|\s(%s))?\s*(%s))? ) ''' % (day_names, re_ordinal, month_names, re_year, re_separator, re_time), (re.VERBOSE | re.IGNORECASE) ), lambda m, base_date: datetime( int(m.group('year') if m.group('year') else base_date.year), HASHMONTHS[m.group('month').strip().lower()], int(m.group('day') if m.group('day') else 1), ) + timedelta(**convert_time_to_hour_minute( m.group('hour'), m.group('minute'), m.group('convention') )) ), ( re.compile( r''' ( ((?P%s)[,\s][-\s]*)? #Matches Monday, Jan 12 2012, Jan 12 2012 etc (?P%s) # Matches any month name [-\s] # Space ((?P\d{1,2})) # Matches a digit (%s)? ([-\s](?P%s))? # Year ((\s|,\s|\s(%s))?\s*(%s))? ) ''' % (day_names, month_names, re_ordinal, re_year, re_separator, re_time), (re.VERBOSE | re.IGNORECASE) ), lambda m, base_date: datetime( int(m.group('year') if m.group('year') else base_date.year), HASHMONTHS[m.group('month').strip().lower()], int(m.group('day') if m.group('day') else 1) ) + timedelta(**convert_time_to_hour_minute( m.group('hour'), m.group('minute'), m.group('convention') )) ), ( re.compile( r''' ( (?P%s) # Matches any month name [-\s] # One or more space (?P\d{1,2}) # Matches a digit (%s)? [-\s]\s*? (?P%s) # Year ((\s|,\s|\s(%s))?\s*(%s))? ) ''' % (month_names, re_ordinal, re_year, re_separator, re_time), (re.VERBOSE | re.IGNORECASE) ), lambda m, base_date: datetime( int(m.group('year') if m.group('year') else base_date.year), HASHMONTHS[m.group('month').strip().lower()], int(m.group('day') if m.group('day') else 1), ) + timedelta(**convert_time_to_hour_minute( m.group('hour'), m.group('minute'), m.group('convention') )) ), ( re.compile( r''' ( ((?P\d+|(%s[-\s]?)+)\s)? # Matches any number or string 25 or twenty five (?P%s)s?\s # Matches days, months, years, weeks, minutes (?P%s) # before, after, earlier, later, ago, from now (\s*(?P(%s)))? ((\s|,\s|\s(%s))?\s*(%s))? ) ''' % (numbers, re_dmy, re_duration, day_nearest_names, re_separator, re_time), (re.VERBOSE | re.IGNORECASE) ), lambda m, base_date: date_from_duration( base_date, m.group('number'), m.group('unit').lower(), m.group('duration').lower(), m.group('base_time') ) + timedelta(**convert_time_to_hour_minute( m.group('hour'), m.group('minute'), m.group('convention') )) ), ( re.compile( r''' ( (?P%s) # First quarter of 2014 \s+ quarter\sof \s+ (?P%s) ) ''' % (re_ordinal, re_year), (re.VERBOSE | re.IGNORECASE) ), lambda m, base_date: date_from_quarter( base_date, HASHORDINALS[m.group('ordinal').lower()], int(m.group('year') if m.group('year') else base_date.year) ) ), ( re.compile( r''' ( (?P\d+) (?P%s) # 1st January 2012 ((\s|,\s|\s(%s))?\s*)? (?P%s) ([,\s]\s*(?P%s))? ) ''' % (re_ordinal, re_separator, month_names, re_year), (re.VERBOSE | re.IGNORECASE) ), lambda m, base_date: datetime( int(m.group('year') if m.group('year') else base_date.year), int(HASHMONTHS[m.group('month').lower()] if m.group('month') else 1), int(m.group('ordinal_value') if m.group('ordinal_value') else 1), ) ), ( re.compile( r''' ( (?P%s) \s+ (?P\d+) (?P%s) # January 1st 2012 ([,\s]\s*(?P%s))? ) ''' % (month_names, re_ordinal, re_year), (re.VERBOSE | re.IGNORECASE) ), lambda m, base_date: datetime( int(m.group('year') if m.group('year') else base_date.year), int(HASHMONTHS[m.group('month').lower()] if m.group('month') else 1), int(m.group('ordinal_value') if m.group('ordinal_value') else 1), ) ), ( re.compile( r''' (?P