replace python django backend with nodejs backend

This commit is contained in:
wea_ondara
2024-05-27 18:59:58 +02:00
parent ebd0748894
commit 8b60d023e8
123 changed files with 15193 additions and 88 deletions

5
backend_old/README.md Normal file
View File

@@ -0,0 +1,5 @@
Run dev env: `./dev.sh`
Migrate `python manage.py migrate`
Make migration `python manage.py makemigrations --name <name> ai`

0
backend_old/__init__.py Normal file
View File

View File

3
backend_old/ai/admin.py Normal file
View File

@@ -0,0 +1,3 @@
from django.contrib import admin
# Register your models here.

View File

@@ -0,0 +1,51 @@
import datetime
import json
import os
import torch
from transformers import AutoModelForCausalLM, AutoTokenizer
class AiBase:
model_id_or_path: str = None
model = None
tokenizer = None
def __init__(self, model_id_or_path: str = None):
self.model_id_or_path = model_id_or_path
print('Loading ' + model_id_or_path)
self.model_id_or_path = model_id_or_path
self.model = AutoModelForCausalLM.from_pretrained(model_id_or_path, torch_dtype='auto', device_map='auto')
self.tokenizer = AutoTokenizer.from_pretrained(model_id_or_path)
# print(self.tokenizer.default_chat_template)
# print(type(self.model))
# print(type(self.tokenizer))
print('Loaded')
def generate(self, messages):
return []
def record_conversation(self, messages, response):
messages = messages + [response]
this_dir = os.path.dirname(os.path.abspath(__file__))
conversations_folder = this_dir + '/../../../conversations'
folder = conversations_folder + '/' + self.model_id_or_path.replace('/', '_')
self._mkdir(conversations_folder)
self._mkdir(folder)
timestamp = datetime.datetime.utcnow().strftime('%Y%m%d%H%M%S')
pickle_filename = folder + '/' + timestamp + '.json'
with open(pickle_filename, 'w') as file:
json.dump(messages, file)
def _mkdir(self, path):
if not os.path.isdir(path):
os.mkdir(path)
def __del__(self):
del self.model
del self.tokenizer
import gc
gc.collect()
torch.cuda.empty_cache()

View File

@@ -0,0 +1,38 @@
import torch
from .AiBase import AiBase
class QwenAi(AiBase):
default_device = 'cuda' # the device to load the model onto
default_model_id = 'Qwen/Qwen1.5-1.8B-Chat'
default_instruction = {'role': 'system',
'name': 'system',
'content': 'Your name is "Laura". You are an AI created by Alice.'}
def __init__(self, model_id_or_path=default_model_id):
super().__init__(model_id_or_path)
def generate(self, messages):
try:
# prepare
messages = [m for m in messages if m['role'] != 'system']
input_messages = [self.default_instruction] + messages
# generate
text = self.tokenizer.apply_chat_template(input_messages, tokenize=False, add_generation_prompt=True)
model_inputs = self.tokenizer([text], return_tensors='pt').to(self.default_device)
generated_ids = self.model.generate(model_inputs.input_ids, max_new_tokens=300)
generated_ids = [
output_ids[len(input_ids):] for input_ids, output_ids in zip(model_inputs.input_ids, generated_ids)
]
response = self.tokenizer.batch_decode(generated_ids, skip_special_tokens=True)[0]
# add response and save conversation
response_entry = {'role': 'assistant', 'name': 'assistant', 'content': response}
messages.append(response_entry)
self.record_conversation(input_messages, response_entry)
return messages
finally:
torch.cuda.empty_cache() # clear cache or the gpu mem will be used a lot

View File

11
backend_old/ai/apps.py Normal file
View File

@@ -0,0 +1,11 @@
from django.apps import AppConfig
class AisConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'ai'
def ready(self):
# noinspection PyUnresolvedReferences
from . import events # register events
pass

View File

@@ -0,0 +1,46 @@
from injector import Injector
from .eventsold import ChatTextEvent
from .websocket import DashboardConnectionManager
class EventListener:
def handle(self, *args, **kwargs):
raise NotImplementedError
class ChatTextListener(EventListener):
def handle(self, event):
print('ChatTextListener')
get_manager().broadcast({'type': 'chat_text', 'message': event.message, 'ai_instance': event.ai_name})
def get_manager():
from .injector import InjectorModule
inj = Injector(InjectorModule)
return inj.get(DashboardConnectionManager)
ChatTextEvent.register(ChatTextListener())
# @injector.inject
# def on_chat_text(manager: DashboardConnectionManager, ai_instance: AiInstance, message):
# manager.broadcast({'type': 'chat_text', 'message': message, 'ai_instance': ai_instance.configuration.name})
#
#
# @injector.inject
# def on_discord_online(manager: DashboardConnectionManager, ai_instance: AiInstance, status: bool):
# manager.broadcast({'type': 'discord_online', 'status': status, 'ai_instance': ai_instance.configuration.name})
#
#
# @injector.inject
# def on_discord_react_to_chat(manager: DashboardConnectionManager, ai_instance: AiInstance, status: bool):
# manager.broadcast({'type': 'discord_react_to_chat',
# 'status': status,
# 'ai_instance': ai_instance.configuration.name})
#
# def register_events():
# Dispatcher.RegisterHandler('event::chat_text', on_chat_text)
# Dispatcher.RegisterHandler('event::discord::online', on_discord_online)
# Dispatcher.RegisterHandler('event::discord::react_to_chat', on_discord_react_to_chat)

30
backend_old/ai/events.py Normal file
View File

@@ -0,0 +1,30 @@
from django.dispatch import receiver, Signal
from injector import Injector
from .websocket import DashboardConnectionManager
chat_text_signal = Signal()
discord_online_signal = Signal()
discord_react_to_chat_signal = Signal()
@receiver(chat_text_signal)
def handle_chat_text(sender, **kwargs):
get_manager().broadcast({'type': 'chat_text', 'message': kwargs['message'], 'ai_instance': kwargs['ai_name']})
@receiver(discord_online_signal)
def handle_discord_online(sender, **kwargs):
get_manager().broadcast({'type': 'discord_online', 'status': kwargs['status'], 'ai_instance': kwargs['ai_name']})
@receiver(discord_react_to_chat_signal)
def handle_discord_react_to_chat(sender, **kwargs):
get_manager().broadcast(
{'type': 'discord_react_to_chat', 'status': kwargs['status'], 'ai_instance': kwargs['ai_name']})
def get_manager():
from .injector import InjectorModule
inj = Injector(InjectorModule)
return inj.get(DashboardConnectionManager)

View File

@@ -0,0 +1,11 @@
from injector import Binder, Module, singleton
from .services.AiService import AiService
from .websocket import DashboardConnectionManager
class InjectorModule(Module):
def configure(self, binder: Binder) -> None:
self.__class__.instance = self
binder.bind(AiService, scope=singleton)
binder.bind(DashboardConnectionManager, scope=singleton)

View File

@@ -0,0 +1,22 @@
# Generated by Django 5.0.4 on 2024-04-22 15:06
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
]
operations = [
migrations.CreateModel(
name='AiConfig',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=255)),
('model_id_or_path', models.CharField(max_length=255)),
],
),
]

View File

@@ -0,0 +1,19 @@
# Generated by Django 5.0.4 on 2024-04-23 17:38
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('ai', '0001_initial'),
]
operations = [
migrations.AddField(
model_name='aiconfig',
name='discord_token',
field=models.CharField(default='', max_length=255),
preserve_default=False,
),
]

View File

11
backend_old/ai/models.py Normal file
View File

@@ -0,0 +1,11 @@
from django.db import models
# Create your models here.
class AiConfig(models.Model):
name = models.CharField(max_length=255)
model_id_or_path = models.CharField(max_length=255)
discord_token = models.CharField(max_length=255)
def __str__(self):
return self.name

View File

@@ -0,0 +1,19 @@
from rest_framework import serializers
from .models import AiConfig
class AiConfigSerializer(serializers.ModelSerializer):
class Meta:
model = AiConfig
fields = ['name', 'model_id_or_path']
class DiscordStatusSerializer(serializers.Serializer):
online = serializers.BooleanField()
react_to_chat = serializers.BooleanField()
class AiInstanceSerializer(serializers.Serializer):
configuration = AiConfigSerializer()
messages = serializers.ListField()
discord = DiscordStatusSerializer()

View File

@@ -0,0 +1,83 @@
import threading
from typing import AnyStr
from .DcClient import DcClient
from ..ais.AiBase import AiBase
from ..ais.QwenAi import QwenAi
from ..events import chat_text_signal, discord_online_signal, discord_react_to_chat_signal
from ..models import AiConfig
class DiscordStatus:
def __init__(self, instance):
self._instance = instance
self._client = DcClient(instance)
self.online = False
self.react_to_chat = False
def set_online(self, online: bool):
if online and not self.online:
self._client.connect()
elif not online and self.online:
self._client.disconnect()
else:
return
self.online = online
discord_online_signal.send(sender=self.__class__, ai_name=self._instance.configuration.name, status=self.online)
def set_react_to_chat(self, react_to_chat: bool):
self.react_to_chat = react_to_chat
discord_react_to_chat_signal.send(sender=self.__class__, ai_name=self._instance.configuration.name,
status=self.react_to_chat)
def __del__(self):
self.set_online(False)
class AiInstance:
def __init__(self, configuration: AiConfig):
print('Initializing AiInstance ' + configuration.name)
self._bot: AiBase | None = None
self._botLock = threading.Lock()
self.configuration = configuration
self.discord = DiscordStatus(self)
self.messages = []
self._messagesLock = threading.Lock()
def endure_loaded(self):
self._botLock.acquire()
try:
if self._bot is None:
self._bot = QwenAi(self.configuration.model_id_or_path)
finally:
self._botLock.release()
def endure_unloaded(self):
self._botLock.acquire()
try:
if self._bot is not None:
del self._bot
self._bot = None
finally:
self._botLock.release()
def chat_text(self, user: AnyStr, text: AnyStr):
self.endure_loaded()
self._messagesLock.acquire()
try:
self.messages.append({'role': 'user', 'name': user, 'content': text})
chat_text_signal.send(sender=self.__class__, ai_name=self.configuration.name, message=self.messages[-1])
self.messages = self._bot.generate(self.messages)
chat_text_signal.send(sender=self.__class__, ai_name=self.configuration.name, message=self.messages[-1])
return self.messages[-1]
finally:
self._messagesLock.release()
def chat_conversation(self, conversation):
self.endure_loaded()
return self._bot.generate(conversation)
def __del__(self):
print('Destructing AiInstance ' + self.configuration.name)
del self._bot
del self.discord

View File

@@ -0,0 +1,34 @@
from .AiInstance import AiInstance
from ..models import AiConfig
class AiService:
# Singleton
# def __new__(cls):
# if not hasattr(cls, 'instance'):
# cls.instance = super(AiService, cls).__new__(cls)
# return cls.instance
def __init__(self):
self.instances: [AiInstance] = []
print('Initializing AiService')
def get_configurations(self) -> [AiConfig]:
return AiConfig.objects.filter()
def get_instances(self) -> [AiInstance]:
self.__ensure_instances_inited__()
return self.instances
def get_instance(self, name: str) -> AiInstance:
return [i for i in self.get_instances() if i.configuration.name == name][0]
def __ensure_instances_inited__(self):
for config in self.get_configurations():
if len([i for i in self.instances if i.configuration.name == config.name]) == 0:
self.instances.append(AiInstance(config))
def __del__(self):
print('Destructing AiService')
for instance in self.instances:
del instance

View File

@@ -0,0 +1,44 @@
from threading import Thread
from discord import Intents, Client
from . import AiInstance
class DcClient:
def __init__(self, _ai_instance: AiInstance):
self._ai_instance = _ai_instance
self._intents = Intents.default()
self._intents.message_content = True
self._discord_client = Client(intents=self._intents)
@self._discord_client.event
async def on_ready():
print(f'We have logged in as {self._discord_client.user}')
@self._discord_client.event
async def on_message(message):
if not self._ai_instance.discord.react_to_chat:
return None
if message.author == self._discord_client.user:
return
if message.content.isspace():
await message.channel.send('### Empty message')
return
response = self._ai_instance.chat_text(message.author.name, message.content)
if response is not None:
await message.channel.send(response['content'])
def connect(self):
def start():
self._discord_client.run(self._ai_instance.configuration.discord_token)
thread = Thread(target=start)
thread.start()
def disconnect(self):
self._discord_client.close()

3
backend_old/ai/tests.py Normal file
View File

@@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

18
backend_old/ai/urls.py Normal file
View File

@@ -0,0 +1,18 @@
from channels.routing import ProtocolTypeRouter, URLRouter
from django.urls import path
from .views import AiConfigApiView, AiApiView, AiDiscordApiView, AiChatTextApiView
from .websocket import DashboardConsumer
urlpatterns = [
path('api/v1/ai/<name>/chat/text', AiChatTextApiView.as_view()),
path('api/v1/ai/<name>/discord', AiDiscordApiView.as_view()),
path('api/v1/ai/config', AiConfigApiView.as_view()),
path('api/v1/ai', AiApiView.as_view()),
]
application = ProtocolTypeRouter({
'websocket': URLRouter([
path('ws/dashboard/', DashboardConsumer.as_asgi()),
])
})

87
backend_old/ai/views.py Normal file
View File

@@ -0,0 +1,87 @@
from injector import inject
from rest_framework import status
from rest_framework.response import Response
from rest_framework.views import APIView
from .serializers import AiConfigSerializer, AiInstanceSerializer
from .services.AiService import AiService
class AiApiView(APIView):
# add permission to check if user is authenticated
# permission_classes = [permissions.IsAuthenticated]
ai_service: AiService = None
@inject
def setup(self, request, ai_service: AiService, **kwargs):
super().setup(request)
self.ai_service = ai_service
def get(self, request, *args, **kwargs):
instances = self.ai_service.get_instances()
serializer = AiInstanceSerializer(instances, many=True)
return Response(serializer.data, status=status.HTTP_200_OK)
class AiConfigApiView(APIView):
# add permission to check if user is authenticated
# permission_classes = [permissions.IsAuthenticated]
ai_service: AiService = None
@inject
def setup(self, request, ai_service: AiService, **kwargs):
super().setup(request)
self.ai_service = ai_service
def get(self, request, *args, **kwargs):
configurations = self.ai_service.get_configurations()
serializer = AiConfigSerializer(configurations, many=True)
return Response(serializer.data, status=status.HTTP_200_OK)
class AiChatTextApiView(APIView):
# add permission to check if user is authenticated
# permission_classes = [permissions.IsAuthenticated]
ai_service: AiService = None
@inject
def setup(self, request, ai_service: AiService, **kwargs):
super().setup(request)
self.ai_service = ai_service
def post(self, request, *args, **kwargs):
name = self.kwargs.get('name')
print("name", name)
ai_instance = self.ai_service.get_instance(name)
if ai_instance is None:
return Response(status=status.HTTP_404_NOT_FOUND)
print(request.data)
response = ai_instance.chat_text(request.data['name'], request.data['content'])
return Response(response, status=status.HTTP_200_OK)
class AiDiscordApiView(APIView):
# add permission to check if user is authenticated
# permission_classes = [permissions.IsAuthenticated]
ai_service: AiService = None
@inject
def setup(self, request, ai_service: AiService, **kwargs):
super().setup(request)
self.ai_service = ai_service
def patch(self, request, *args, **kwargs):
name = self.kwargs.get('name')
ai_instance = self.ai_service.get_instance(name)
if ai_instance is None:
return Response(status=status.HTTP_404_NOT_FOUND)
if 'online' in request.data:
ai_instance.discord.set_online(request.data['online'])
if 'react_to_chat' in request.data:
ai_instance.discord.set_react_to_chat(request.data['react_to_chat'])
return Response(status=status.HTTP_200_OK)

View File

@@ -0,0 +1,44 @@
import injector
import json
from channels.generic.websocket import WebsocketConsumer
class DashboardConnectionManager:
def __init__(self):
self._clients = []
def register(self, client: WebsocketConsumer):
self._clients.append(client)
pass
def unregister(self, client: WebsocketConsumer):
self._clients.remove(client)
def broadcast(self, message: any):
for client in self._clients:
client.push(message)
class DashboardConsumer(WebsocketConsumer):
@injector.inject
def __init__(self, manager: DashboardConnectionManager, *args, **kwargs):
super().__init__(*args, **kwargs)
self._manager = manager
def connect(self):
self.accept()
self._manager.register(self)
def disconnect(self, close_code):
self._manager.unregister(self)
def receive(self, text_data=None, bytes_data=None):
# text_data_json = json.loads(text_data)
# message = text_data_json['message']
# self.send(text_data=json.dumps({
# 'message': message
# }))
pass # do nothing currently
def push(self, message):
self.send(text_data=json.dumps(message))

View File

View File

@@ -0,0 +1,16 @@
"""
ASGI config for backend project.
It exposes the ASGI callable as a module-level variable named ``application``.
For more information on this file, see
https://docs.djangoproject.com/en/5.0/howto/deployment/asgi/
"""
import os
from django.core.asgi import get_asgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'backend.settings')
application = get_asgi_application()

View File

@@ -0,0 +1,132 @@
"""
Django settings for backend project.
Generated by 'django-admin startproject' using Django 5.0.4.
For more information on this file, see
https://docs.djangoproject.com/en/5.0/topics/settings/
For the full list of settings and their values, see
https://docs.djangoproject.com/en/5.0/ref/settings/
"""
from pathlib import Path
# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/5.0/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = 'django-insecure-a*pam^5+0)v!(#rz^s4cmi13iv==4_!8(v#xk%qnqkv+7z=rai'
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True
ALLOWED_HOSTS = []
# Application definition
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'django_injector',
'rest_framework',
'ai',
'channels',
]
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
# 'django_injector.inject_request_middleware',
]
CHANNEL_LAYERS = {
'default': {
'BACKEND': 'channels.layers.InMemoryChannelLayer',
},
}
ROOT_URLCONF = 'backend.urls'
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
],
},
},
]
WSGI_APPLICATION = 'backend.wsgi.application'
# Database
# https://docs.djangoproject.com/en/5.0/ref/settings/#databases
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': BASE_DIR / 'db.sqlite3',
}
}
# Password validation
# https://docs.djangoproject.com/en/5.0/ref/settings/#auth-password-validators
AUTH_PASSWORD_VALIDATORS = [
{
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
},
]
# Internationalization
# https://docs.djangoproject.com/en/5.0/topics/i18n/
LANGUAGE_CODE = 'en-us'
TIME_ZONE = 'UTC'
USE_I18N = True
USE_TZ = True
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/5.0/howto/static-files/
STATIC_URL = 'static/'
# Default primary key field type
# https://docs.djangoproject.com/en/5.0/ref/settings/#default-auto-field
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
INJECTOR_MODULES = [
'ai.injector.InjectorModule',
]

View File

@@ -0,0 +1,23 @@
"""
URL configuration for backend project.
The `urlpatterns` list routes URLs to views. For more information please see:
https://docs.djangoproject.com/en/5.0/topics/http/urls/
Examples:
Function views
1. Add an import: from my_app import views
2. Add a URL to urlpatterns: path('', views.home, name='home')
Class-based views
1. Add an import: from other_app.views import Home
2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
Including another URLconf
1. Import the include() function: from django.urls import include, path
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
"""
from django.contrib import admin
from django.urls import path, include
urlpatterns = [
path('admin/', admin.site.urls),
path('', include('ai.urls')),
]

View File

@@ -0,0 +1,16 @@
"""
WSGI config for backend project.
It exposes the WSGI callable as a module-level variable named ``application``.
For more information on this file, see
https://docs.djangoproject.com/en/5.0/howto/deployment/wsgi/
"""
import os
from django.core.wsgi import get_wsgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'backend.settings')
application = get_wsgi_application()

BIN
backend_old/db.sqlite3 Normal file

Binary file not shown.

1
backend_old/dev.sh Executable file
View File

@@ -0,0 +1 @@
python manage.py runserver 0.0.0.0:8877

22
backend_old/manage.py Executable file
View File

@@ -0,0 +1,22 @@
#!/usr/bin/env python
"""Django's command-line utility for administrative tasks."""
import os
import sys
def main():
"""Run administrative tasks."""
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'backend.settings')
try:
from django.core.management import execute_from_command_line
except ImportError as exc:
raise ImportError(
"Couldn't import Django. Are you sure it's installed and "
"available on your PYTHONPATH environment variable? Did you "
"forget to activate a virtual environment?"
) from exc
execute_from_command_line(sys.argv)
if __name__ == '__main__':
main()