added backend which maintains ai instances;

added frontend as control panel
This commit is contained in:
wea_ondara
2024-04-24 19:40:03 +02:00
parent 9cafd8b1e9
commit 41474d474c
79 changed files with 9022 additions and 0 deletions

0
backend/ai/__init__.py Normal file
View File

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

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

51
backend/ai/ais/AiBase.py Normal file
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()

38
backend/ai/ais/QwenAi.py Normal file
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

6
backend/ai/apps.py Normal file
View File

@@ -0,0 +1,6 @@
from django.apps import AppConfig
class AisConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'ai'

8
backend/ai/injector.py Normal file
View File

@@ -0,0 +1,8 @@
from injector import Binder, Module, singleton
from .services.AiService import AiService
class InjectorModule(Module):
def configure(self, binder: Binder) -> None:
binder.bind(AiService, to=AiService(), 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/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

19
backend/ai/serializers.py Normal file
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,68 @@
from typing import AnyStr
from .DcClient import DcClient
from ..ais.AiBase import AiBase
from ..ais.QwenAi import QwenAi
from ..models import AiConfig
class DiscordStatus:
_instance: any
_client: DcClient
online: bool = False
react_to_chat: bool = False
def __init__(self, instance):
self._instance = instance
self._client = DcClient(instance)
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
def set_react_to_chat(self, react_to_chat: bool):
self.react_to_chat = react_to_chat
def __del__(self):
self.set_online(False)
class AiInstance:
_bot: AiBase | None = None
configuration: AiConfig
discord: DiscordStatus
messages = []
def __init__(self, configuration: AiConfig):
print('Initializing AiInstance ' + configuration.name)
self.configuration = configuration
self.discord = DiscordStatus(self)
def endure_loaded(self):
if self._bot is None:
self._bot = QwenAi(self.configuration.model_id_or_path)
def endure_unloaded(self):
if self._bot is not None:
del self._bot
self._bot = None
def chat_text(self, user: AnyStr, text: AnyStr):
self.endure_loaded()
self.messages.append({'role': 'user', 'name': user, 'content': text})
self.messages = self._bot.generate(self.messages)
return self.messages[-1]
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,35 @@
from .AiInstance import AiInstance
from ..models import AiConfig
class AiService:
instances: [AiInstance] = []
# Singleton
# def __new__(cls):
# if not hasattr(cls, 'instance'):
# cls.instance = super(AiService, cls).__new__(cls)
# return cls.instance
def __init__(self):
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,49 @@
from threading import Thread
from discord import Intents, Client
from . import AiInstance
class DcClient:
_intents: Intents
_discord_client: Client
_ai_instance: AiInstance
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/ai/tests.py Normal file
View File

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

10
backend/ai/urls.py Normal file
View File

@@ -0,0 +1,10 @@
from django.urls import path
from .views import AiConfigApiView, AiApiView, AiDiscordApiView, AiChatTextApiView
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()),
]

87
backend/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)