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
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.
[](https://pypi.python.org/pypi/chatterbot/)
[](https://www.python.org/downloads/release/python-360/)
[](https://coveralls.io/r/gunthercox/ChatterBot)
[](https://bsky.app/profile/chatterbot.us)
[](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