├── .gitignore ├── Dockerfile ├── Procfile ├── README.md ├── app.json ├── chat ├── __init__.py ├── admin.py ├── apps.py ├── consumers.py ├── forms.py ├── migrations │ └── __init__.py ├── models.py ├── routing.py ├── tasks.py ├── templates │ ├── base.html │ └── chat │ │ └── chat.html ├── tests.py ├── urls.py └── views.py ├── docker-compose.yml ├── manage.py ├── project ├── __init__.py ├── asgi.py ├── celery.py ├── routing.py ├── settings.py ├── urls.py └── wsgi.py ├── requirements.txt └── runtime.txt /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__/ 2 | db.sqlite3 3 | *.env 4 | *.rdb 5 | .vscode/ 6 | venv/ 7 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.11.4 2 | 3 | ENV PYTHONUNBUFFERED 1 4 | 5 | RUN mkdir /app 6 | WORKDIR /app 7 | ADD requirements.txt /app/ 8 | 9 | RUN pip install -U pip 10 | 11 | RUN pip install -r requirements.txt 12 | 13 | ADD . /app/ 14 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | release: python manage.py migrate --noinput 2 | web: daphne project.asgi:application --port $PORT --bind 0.0.0.0 3 | worker: celery -A project worker --loglevel info 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Django Chatbot 2 | 3 | The Django Chatbot is a chatbot application that utilizes background tasks processing and communication via WebSockets. 4 | 5 | For more detailed information, please refer to my article titled [Heroku Chatbot with Celery, WebSockets, and Redis](https://itnext.io/heroku-chatbot-with-celery-websockets-and-redis-340fcd160f06). 6 | 7 | ## Deployment 8 | ### Docker 9 | 10 | To deploy the application using Docker, follow these steps: 11 | 1. Clone the repository: `git clone https://github.com/slyapustin/django-chatbot` 12 | 2. Build and run the project: `docker-compose up` 13 | 3. Visit `http://localhost:8000/` in your web browser. 14 | 15 | ### Heroku 16 | Alternatively, you can host the chatbot on [Heroku](https://www.heroku.com). Please note that an [account verification](https://devcenter.heroku.com/articles/account-verification) is required. 17 | 18 | Click the button below to deploy the chatbot on Heroku: 19 | 20 | [![Deploy](https://www.herokucdn.com/deploy/button.svg)](https://heroku.com/deploy) 21 | 22 | ## Technology Stack 23 | The chatbot utilizes the following technologies: 24 | 25 | - [Django](https://www.djangoproject.com/): The main web framework. 26 | - [Django Channels](https://github.com/django/channels): The WebSocket framework. 27 | - [Celery](http://www.celeryproject.org/): An asynchronous task queue. 28 | - [Redis](https://redis.io/): A message broker and cache backend. 29 | - [Daphne](https://github.com/django/daphne): An HTTP and WebSocket protocol server. 30 | - [Heroku](https://www.heroku.com): The hosting platform. 31 | 32 | ## Supported commands 33 | The chatbot supports the following commands: 34 | 35 | - `sum `: Calculates the sum of two integers. 36 | - `status `: Checks the status of a website. 37 | 38 | ## Need some help? 39 | Join the Slack channel [#django-chatbot](https://join.slack.com/t/lyapustin/shared_invite/enQtNzc0MDQ0NjMxMzY2LTNmOTQ0NWM3YTQxYjM2ZGM3NTZiZWE1Y2E4ZGYyNDc2ODc3NzQ3ZWNlNDk3MGEyMWU0MDFiM2ZlYjYzY2I2Zjk) for assistance. -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Django Chatbot", 3 | "description": "Django Chatbot application with the background tasks processing and communications via WebSockets.", 4 | "keywords": [ 5 | "celery", 6 | "django", 7 | "heroku", 8 | "daphne", 9 | "channels", 10 | "django-channels", 11 | "chatbot", 12 | "websockets" 13 | ], 14 | "website": "https://github.com/slyapustin/django-chatbot/", 15 | "repository": "https://github.com/slyapustin/django-chatbot/", 16 | "env": { 17 | "DJANGO_SECRET_KEY": { 18 | "description": "A secret key for a particular Django installation.", 19 | "generator": "secret" 20 | } 21 | }, 22 | "formation": { 23 | "web": { 24 | "quantity": 1, 25 | "size": "free" 26 | }, 27 | "worker": { 28 | "quantity": 1, 29 | "size": "free" 30 | } 31 | }, 32 | "buildpacks": [ 33 | { 34 | "url": "heroku/python" 35 | } 36 | ], 37 | "addons": [ 38 | { 39 | "plan": "heroku-postgresql" 40 | }, 41 | { 42 | "plan": "heroku-redis" 43 | } 44 | ] 45 | } 46 | -------------------------------------------------------------------------------- /chat/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Phoenix-Genius/django-chatbot/360585b21e9e508ec3234b5322c03ce884caf7ef/chat/__init__.py -------------------------------------------------------------------------------- /chat/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | # Register your models here. 4 | -------------------------------------------------------------------------------- /chat/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class ChatConfig(AppConfig): 5 | name = "chat" 6 | -------------------------------------------------------------------------------- /chat/consumers.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | from asgiref.sync import async_to_sync 4 | from channels.generic.websocket import WebsocketConsumer 5 | 6 | from . import tasks 7 | 8 | COMMANDS = { 9 | "help": { 10 | "help": "Display help message.", 11 | }, 12 | "sum": { 13 | "args": 2, 14 | "help": "Calculate sum of two integer arguments. Example: `sum 12 32`.", 15 | "task": "add", 16 | }, 17 | "status": { 18 | "args": 1, 19 | "help": "Check website status. Example: `status twitter.com`.", 20 | "task": "url_status", 21 | }, 22 | } 23 | 24 | 25 | class ChatConsumer(WebsocketConsumer): 26 | def receive(self, text_data): 27 | text_data_json = json.loads(text_data) 28 | message = text_data_json["message"] 29 | 30 | response_message = "Please type `help` for the list of the commands." 31 | message_parts = message.split() 32 | if message_parts: 33 | command = message_parts[0].lower() 34 | if command == "help": 35 | response_message = "List of the available commands:\n" + "\n".join( 36 | [ 37 | f'{command} - {params["help"]} ' 38 | for command, params in COMMANDS.items() 39 | ] 40 | ) 41 | elif command in COMMANDS: 42 | if len(message_parts[1:]) != COMMANDS[command]["args"]: 43 | response_message = f"Wrong arguments for the command `{command}`." 44 | else: 45 | getattr(tasks, COMMANDS[command]["task"]).delay( 46 | self.channel_name, *message_parts[1:] 47 | ) 48 | response_message = f"Command `{command}` received." 49 | 50 | async_to_sync(self.channel_layer.send)( 51 | self.channel_name, {"type": "chat_message", "message": response_message} 52 | ) 53 | 54 | def chat_message(self, event): 55 | message = event["message"] 56 | 57 | # Send message to WebSocket 58 | self.send(text_data=json.dumps({"message": f"[bot]: {message}"})) 59 | -------------------------------------------------------------------------------- /chat/forms.py: -------------------------------------------------------------------------------- 1 | from django import forms 2 | from celery import group 3 | from .tasks import add 4 | 5 | 6 | class AddForm(forms.Form): 7 | a = forms.IntegerField() 8 | b = forms.IntegerField() 9 | 10 | number = forms.IntegerField(label="Number of tasks", min_value=1, max_value=100000) 11 | 12 | def create_calculate_task(self): 13 | return group( 14 | add.s(self.cleaned_data["a"], self.cleaned_data["b"]) 15 | for i in range(self.cleaned_data["number"]) 16 | )() 17 | -------------------------------------------------------------------------------- /chat/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Phoenix-Genius/django-chatbot/360585b21e9e508ec3234b5322c03ce884caf7ef/chat/migrations/__init__.py -------------------------------------------------------------------------------- /chat/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | # Create your models here. 4 | -------------------------------------------------------------------------------- /chat/routing.py: -------------------------------------------------------------------------------- 1 | from django.urls import re_path 2 | from chat import consumers 3 | 4 | websocket_urlpatterns = [ 5 | re_path(r"^ws/chat/$", consumers.ChatConsumer.as_asgi()), 6 | ] 7 | -------------------------------------------------------------------------------- /chat/tasks.py: -------------------------------------------------------------------------------- 1 | import requests 2 | from asgiref.sync import async_to_sync 3 | from celery import shared_task 4 | from channels.layers import get_channel_layer 5 | from django.core.cache import cache 6 | 7 | channel_layer = get_channel_layer() 8 | 9 | 10 | @shared_task 11 | def add(channel_name, x, y): 12 | message = "{}+{}={}".format(x, y, int(x) + int(y)) 13 | async_to_sync(channel_layer.send)( 14 | channel_name, {"type": "chat.message", "message": message} 15 | ) 16 | 17 | 18 | @shared_task 19 | def url_status(channel_name, url): 20 | if not url.startswith("http"): 21 | url = f"https://{url}" 22 | 23 | status = cache.get(url) 24 | if not status: 25 | try: 26 | r = requests.get(url, timeout=10) 27 | status = r.status_code 28 | cache.set(url, status, 60 * 60) 29 | except requests.exceptions.RequestException: 30 | status = "Not available" 31 | 32 | message = f"{url} status is {status}" 33 | async_to_sync(channel_layer.send)( 34 | channel_name, {"type": "chat.message", "message": message} 35 | ) 36 | -------------------------------------------------------------------------------- /chat/templates/base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | {% block title %}Django Chatbot{% endblock %} 6 | 7 | 8 | {% block body %} 9 | {% endblock %} 10 | 11 | {% block scripts%}{% endblock %} 12 | 13 | -------------------------------------------------------------------------------- /chat/templates/chat/chat.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block body %} 4 |
5 |
6 | 7 | {% endblock %} 8 | 9 | {% block scripts%} 10 | 47 | {% endblock %} 48 | -------------------------------------------------------------------------------- /chat/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /chat/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | 3 | from . import views 4 | 5 | urlpatterns = [ 6 | path('', views.ChatView.as_view(), name='chat'), 7 | ] 8 | -------------------------------------------------------------------------------- /chat/views.py: -------------------------------------------------------------------------------- 1 | from django.views.generic import TemplateView 2 | 3 | 4 | class ChatView(TemplateView): 5 | template_name = "chat/chat.html" 6 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.3' 2 | services: 3 | web: 4 | build: . 5 | command: bash -c "python ./manage.py migrate && daphne project.asgi:application --port 8000 --bind 0.0.0.0" 6 | volumes: 7 | - .:/app 8 | ports: 9 | - "8000:8000" 10 | depends_on: 11 | - redis 12 | - postgres 13 | environment: 14 | - REDIS_URL=redis://redis:6379 15 | - DJANGO_SECRET_KEY=keep-it-secret 16 | - DATABASE_URL=postgres://postgres:postgres@postgres:5432/postgres 17 | 18 | worker: 19 | build: . 20 | command: celery -A project worker --loglevel info 21 | volumes: 22 | - .:/app 23 | depends_on: 24 | - web 25 | environment: 26 | - REDIS_URL=redis://redis:6379 27 | - REMAP_SIGTERM=SIGQUIT 28 | - DJANGO_SECRET_KEY=keep-it-secret 29 | - DATABASE_URL=postgres://postgres:postgres@postgres:5432/postgres 30 | 31 | redis: 32 | image: "redis:alpine" 33 | 34 | postgres: 35 | image: "postgres:13.4" 36 | environment: 37 | - POSTGRES_PASSWORD=postgres 38 | -------------------------------------------------------------------------------- /manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Django's command-line utility for administrative tasks.""" 3 | import os 4 | import sys 5 | 6 | 7 | def main(): 8 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'project.settings') 9 | try: 10 | from django.core.management import execute_from_command_line 11 | except ImportError as exc: 12 | raise ImportError( 13 | "Couldn't import Django. Are you sure it's installed and " 14 | "available on your PYTHONPATH environment variable? Did you " 15 | "forget to activate a virtual environment?" 16 | ) from exc 17 | execute_from_command_line(sys.argv) 18 | 19 | 20 | if __name__ == '__main__': 21 | main() 22 | -------------------------------------------------------------------------------- /project/__init__.py: -------------------------------------------------------------------------------- 1 | from project.celery import app as celery_app 2 | -------------------------------------------------------------------------------- /project/asgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | ASGI entrypoint. Configures Django and then runs the application 3 | defined in the ASGI_APPLICATION setting. 4 | """ 5 | 6 | import os 7 | import django 8 | from channels.routing import get_default_application 9 | 10 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "project.settings") 11 | django.setup() 12 | application = get_default_application() 13 | -------------------------------------------------------------------------------- /project/celery.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from celery import Celery 4 | 5 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "project.settings") 6 | 7 | app = Celery("project") 8 | app.config_from_object("django.conf:settings", namespace="CELERY") 9 | app.autodiscover_tasks() 10 | -------------------------------------------------------------------------------- /project/routing.py: -------------------------------------------------------------------------------- 1 | import chat.routing 2 | from channels.routing import ProtocolTypeRouter, URLRouter 3 | from django.core.asgi import get_asgi_application 4 | 5 | application = ProtocolTypeRouter( 6 | { 7 | "http": get_asgi_application(), 8 | "websocket": URLRouter(chat.routing.websocket_urlpatterns), 9 | } 10 | ) 11 | -------------------------------------------------------------------------------- /project/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for project project. 3 | """ 4 | 5 | import os 6 | 7 | import dj_database_url 8 | 9 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...) 10 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 11 | 12 | # SECURITY WARNING: keep the secret key used in production secret! 13 | SECRET_KEY = os.environ['DJANGO_SECRET_KEY'] 14 | 15 | # SECURITY WARNING: don't run with debug turned on in production! 16 | DEBUG = True 17 | 18 | ALLOWED_HOSTS = os.environ.get('ALLOWED_HOSTS', '.herokuapp.com,127.0.0.1,localhost').split(',') 19 | 20 | 21 | # Application definition 22 | 23 | INSTALLED_APPS = [ 24 | 'django.contrib.admin', 25 | 'django.contrib.auth', 26 | 'django.contrib.contenttypes', 27 | 'django.contrib.sessions', 28 | 'django.contrib.messages', 29 | 'django.contrib.staticfiles', 30 | 31 | 'chat', 32 | 'channels', 33 | ] 34 | 35 | MIDDLEWARE = [ 36 | 'whitenoise.middleware.WhiteNoiseMiddleware', 37 | 'django.middleware.security.SecurityMiddleware', 38 | 'django.contrib.sessions.middleware.SessionMiddleware', 39 | 'django.middleware.common.CommonMiddleware', 40 | 'django.middleware.csrf.CsrfViewMiddleware', 41 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 42 | 'django.contrib.messages.middleware.MessageMiddleware', 43 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 44 | ] 45 | 46 | ROOT_URLCONF = 'project.urls' 47 | 48 | TEMPLATES = [ 49 | { 50 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 51 | 'DIRS': [], 52 | 'APP_DIRS': True, 53 | 'OPTIONS': { 54 | 'context_processors': [ 55 | 'django.template.context_processors.debug', 56 | 'django.template.context_processors.request', 57 | 'django.contrib.auth.context_processors.auth', 58 | 'django.contrib.messages.context_processors.messages', 59 | ], 60 | }, 61 | }, 62 | ] 63 | 64 | WSGI_APPLICATION = 'project.wsgi.application' 65 | ASGI_APPLICATION = "project.routing.application" 66 | 67 | # Database 68 | DATABASES = { 69 | 'default': { 70 | 'ENGINE': 'django.db.backends.sqlite3', 71 | 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), 72 | } 73 | } 74 | 75 | # Change 'default' database configuration with $DATABASE_URL. 76 | DATABASES['default'].update(dj_database_url.config()) 77 | 78 | # Password validation 79 | AUTH_PASSWORD_VALIDATORS = [ 80 | { 81 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', 82 | }, 83 | { 84 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 85 | }, 86 | { 87 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', 88 | }, 89 | { 90 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', 91 | }, 92 | ] 93 | 94 | 95 | # Internationalization 96 | LANGUAGE_CODE = 'en-us' 97 | 98 | TIME_ZONE = 'UTC' 99 | 100 | USE_I18N = True 101 | 102 | USE_L10N = True 103 | 104 | USE_TZ = True 105 | 106 | 107 | # Static files (CSS, JavaScript, Images) 108 | STATIC_URL = '/static/' 109 | STATICFILES_STORAGE = 'whitenoise.storage.CompressedManifestStaticFilesStorage' 110 | STATIC_ROOT = os.path.join(BASE_DIR, 'staticfiles') 111 | 112 | CELERY_BROKER_URL = os.environ['REDIS_URL'] 113 | CELERY_RESULT_BACKEND = os.environ['REDIS_URL'] 114 | 115 | CHANNEL_LAYERS = { 116 | 'default': { 117 | 'BACKEND': 'channels_redis.core.RedisChannelLayer', 118 | 'CONFIG': { 119 | "hosts": [os.environ['REDIS_URL']], 120 | }, 121 | }, 122 | } 123 | 124 | CACHES = { 125 | "default": { 126 | "BACKEND": "django_redis.cache.RedisCache", 127 | "LOCATION": os.environ['REDIS_URL'], 128 | "OPTIONS": { 129 | "CLIENT_CLASS": "django_redis.client.DefaultClient" 130 | } 131 | } 132 | } 133 | 134 | SESSION_ENGINE = "django.contrib.sessions.backends.cache" 135 | -------------------------------------------------------------------------------- /project/urls.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from django.urls import path, include 3 | 4 | urlpatterns = [ 5 | path("", include("chat.urls")), 6 | path("admin/", admin.site.urls), 7 | ] 8 | -------------------------------------------------------------------------------- /project/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for project project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/2.2/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'project.settings') 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | celery==5.3.1 2 | channels-redis==4.1.0 3 | channels==4.0.0 4 | daphne==4.0.0 5 | dj-database-url==2.0.0 6 | django-redis==5.3.0 7 | Django==4.2.3 8 | psycopg2-binary==2.9.6 9 | redis==4.6.0 10 | requests==2.31.0 11 | whitenoise==6.5.0 12 | -------------------------------------------------------------------------------- /runtime.txt: -------------------------------------------------------------------------------- 1 | python-3.11.4 2 | --------------------------------------------------------------------------------