diff --git a/README.md b/README.md index 966165f..decefd8 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,40 @@ -# chat_backend +# Chat Bot Backend -Backend django project for the chat site \ No newline at end of file +## Setup + +Clone the repo +```console +git clone http://10.0.0.160:3000/AI_ML_Operations_LLC/Chat_Bot_Backend.git +``` + +Go into the repo +```console +cd Chat_Bot_Backend +``` + +Create the virtual environment +```console +virtualenv --python=python3.9 venv +``` + +Activate the Virtual Environment +```console +. venv/bin/activate +``` + +Install the requirments +```console +python -m pip install -r requirements.txt +``` + +Pre-populate the database with test data + +Run the dev server +```console +python manage.py +``` + +## TODO + + - [ ] Create inital database with temp data + - [ ] Do a lot of stuff..... diff --git a/llm_be/.idea/.gitignore b/llm_be/.idea/.gitignore new file mode 100644 index 0000000..13566b8 --- /dev/null +++ b/llm_be/.idea/.gitignore @@ -0,0 +1,8 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/llm_be/.idea/inspectionProfiles/Project_Default.xml b/llm_be/.idea/inspectionProfiles/Project_Default.xml new file mode 100644 index 0000000..03d9549 --- /dev/null +++ b/llm_be/.idea/inspectionProfiles/Project_Default.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/llm_be/.idea/inspectionProfiles/profiles_settings.xml b/llm_be/.idea/inspectionProfiles/profiles_settings.xml new file mode 100644 index 0000000..105ce2d --- /dev/null +++ b/llm_be/.idea/inspectionProfiles/profiles_settings.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/llm_be/.idea/llm_be.iml b/llm_be/.idea/llm_be.iml new file mode 100644 index 0000000..6a7db59 --- /dev/null +++ b/llm_be/.idea/llm_be.iml @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/llm_be/.idea/misc.xml b/llm_be/.idea/misc.xml new file mode 100644 index 0000000..b808fd5 --- /dev/null +++ b/llm_be/.idea/misc.xml @@ -0,0 +1,7 @@ + + + + + + \ No newline at end of file diff --git a/llm_be/.idea/modules.xml b/llm_be/.idea/modules.xml new file mode 100644 index 0000000..2b64d3d --- /dev/null +++ b/llm_be/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/llm_be/.idea/vcs.xml b/llm_be/.idea/vcs.xml new file mode 100644 index 0000000..6c0b863 --- /dev/null +++ b/llm_be/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/llm_be/chat_backend/__init__.py b/llm_be/chat_backend/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/llm_be/chat_backend/admin.py b/llm_be/chat_backend/admin.py new file mode 100644 index 0000000..b985b0b --- /dev/null +++ b/llm_be/chat_backend/admin.py @@ -0,0 +1,71 @@ +from django.contrib import admin +from .models import CustomUser, Announcement, Company, LLMModels, Conversation, Prompt, Feedback, PromptMetric + +# Register your models here. + + +class AnnouncmentAdmin(admin.ModelAdmin): + model = Announcement + + +class CompanyAdmin(admin.ModelAdmin): + model = Company + + +class CustomUserAdmin(admin.ModelAdmin): + model = CustomUser + list_display = ( + "email", + "username", + "is_company_manager", + "first_name", + "last_name", + "is_active", + "is_staff", + "has_usable_password", + "deleted", + "has_signed_tos", + "last_login", + "slug", + "get_set_password_url" + ) + search_fields = ("fields", "username", "first_name", "last_name", "slug") + +class FeedbackAdmin(admin.ModelAdmin): + model = Feedback + search_fields = ("status", "text", "get_user_email") + list_display= ( + "status", "get_user_email", "title", "category" + ) + +class LLMModelsAdmin(admin.ModelAdmin): + model = LLMModels + list_display = ("name", "port", "description") + search_fields = ("name", "port", "description") + + +class ConversationAdmin(admin.ModelAdmin): + model = Conversation + list_display = ("title", "get_user_email","deleted") + search_fields = ("title",) + + +class PromptAdmin(admin.ModelAdmin): + model = Prompt + list_display = ("message", "user_created", "get_conversation_title") + search_fields = ("message",) + +class PromptMetricAdmin(admin.ModelAdmin): + model = PromptMetric + list_display = ("event", "model_name", "prompt_length","reponse_length",'has_file','file_type', "get_duration") + + +admin.site.register(Announcement, AnnouncmentAdmin) +admin.site.register(Company, CompanyAdmin) +admin.site.register(CustomUser, CustomUserAdmin) + +admin.site.register(LLMModels, LLMModelsAdmin) +admin.site.register(Conversation, ConversationAdmin) +admin.site.register(Prompt, PromptAdmin) +admin.site.register(PromptMetric, PromptMetricAdmin) +admin.site.register(Feedback, FeedbackAdmin) diff --git a/llm_be/chat_backend/apps.py b/llm_be/chat_backend/apps.py new file mode 100644 index 0000000..24f2d5c --- /dev/null +++ b/llm_be/chat_backend/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class ChatBackendConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "chat_backend" diff --git a/llm_be/chat_backend/client.py b/llm_be/chat_backend/client.py new file mode 100644 index 0000000..d550dbb --- /dev/null +++ b/llm_be/chat_backend/client.py @@ -0,0 +1,30 @@ +""" +llama client - Abstract this in the future +""" +import ollama +from typing import List, Dict + +class LlamaClient(object): + def __init__(self, model: str='llama3'): + self.client = ollama.Client(host="http://127.0.0.1:11434") + self.model = model + + def check_if_model_exists(self) -> bool: + raise NotImplementedError + + def generate_conversation_title(self, message:str): + response = self.generate_single_message("Summarise the phrase in one to for words\"%s\"" % message) + + raw_response = response['response'].replace("\"","") + return " ".join(raw_response.split()[:4]) + + def generate_single_message(self, message: str): + return ollama.generate(model=self.model, prompt=message) + + def get_chat_response(self, messages: List[str]): + return self.client.chat(model = self.model, messages=messages, stream=False) + + + def get_streamed_chat_response(self, messages: List[str]): + return self.client.chat(model = self.model, messages=messages, stream=True) + diff --git a/llm_be/chat_backend/migrations/0001_initial.py b/llm_be/chat_backend/migrations/0001_initial.py new file mode 100644 index 0000000..1662b14 --- /dev/null +++ b/llm_be/chat_backend/migrations/0001_initial.py @@ -0,0 +1,158 @@ +# Generated by Django 3.2.18 on 2024-06-20 20:41 + +import django.contrib.auth.models +import django.contrib.auth.validators +from django.db import migrations, models +import django.db.models.deletion +import django.utils.timezone + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ("auth", "0012_alter_user_first_name_max_length"), + ] + + operations = [ + migrations.CreateModel( + name="Company", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("name", models.TextField(max_length=256)), + ("state", models.TextField(max_length=2)), + ("zipcode", models.TextField(max_length=5)), + ("address", models.TextField(max_length=256)), + ], + ), + migrations.CreateModel( + name="CustomUser", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("password", models.CharField(max_length=128, verbose_name="password")), + ( + "last_login", + models.DateTimeField( + blank=True, null=True, verbose_name="last login" + ), + ), + ( + "is_superuser", + models.BooleanField( + default=False, + help_text="Designates that this user has all permissions without explicitly assigning them.", + verbose_name="superuser status", + ), + ), + ( + "username", + models.CharField( + error_messages={ + "unique": "A user with that username already exists." + }, + help_text="Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.", + max_length=150, + unique=True, + validators=[ + django.contrib.auth.validators.UnicodeUsernameValidator() + ], + verbose_name="username", + ), + ), + ( + "first_name", + models.CharField( + blank=True, max_length=150, verbose_name="first name" + ), + ), + ( + "last_name", + models.CharField( + blank=True, max_length=150, verbose_name="last name" + ), + ), + ( + "email", + models.EmailField( + blank=True, max_length=254, verbose_name="email address" + ), + ), + ( + "is_staff", + models.BooleanField( + default=False, + help_text="Designates whether the user can log into this admin site.", + verbose_name="staff status", + ), + ), + ( + "is_active", + models.BooleanField( + default=True, + help_text="Designates whether this user should be treated as active. Unselect this instead of deleting accounts.", + verbose_name="active", + ), + ), + ( + "date_joined", + models.DateTimeField( + default=django.utils.timezone.now, verbose_name="date joined" + ), + ), + ( + "company", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="chat_backend.company", + ), + ), + ( + "groups", + models.ManyToManyField( + blank=True, + help_text="The groups this user belongs to. A user will get all permissions granted to each of their groups.", + related_name="user_set", + related_query_name="user", + to="auth.Group", + verbose_name="groups", + ), + ), + ( + "user_permissions", + models.ManyToManyField( + blank=True, + help_text="Specific permissions for this user.", + related_name="user_set", + related_query_name="user", + to="auth.Permission", + verbose_name="user permissions", + ), + ), + ], + options={ + "verbose_name": "user", + "verbose_name_plural": "users", + "abstract": False, + }, + managers=[ + ("objects", django.contrib.auth.models.UserManager()), + ], + ), + ] diff --git a/llm_be/chat_backend/migrations/0002_accouncement.py b/llm_be/chat_backend/migrations/0002_accouncement.py new file mode 100644 index 0000000..3a99827 --- /dev/null +++ b/llm_be/chat_backend/migrations/0002_accouncement.py @@ -0,0 +1,43 @@ +# Generated by Django 3.2.18 on 2024-06-21 19:27 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("chat_backend", "0001_initial"), + ] + + operations = [ + migrations.CreateModel( + name="Accouncement", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "status", + models.CharField( + choices=[ + ("DEFAULT", "default"), + ("WARNING", "warning"), + ("INFO", "info"), + ("DANGER", "danger"), + ], + default="DEFAULT", + max_length=7, + ), + ), + ("message", models.TextField(max_length=256)), + ("start_date_time", models.DateTimeField(auto_now=True)), + ("end_date_time", models.DateTimeField(auto_now=True)), + ], + ), + ] diff --git a/llm_be/chat_backend/migrations/0003_alter_customuser_company.py b/llm_be/chat_backend/migrations/0003_alter_customuser_company.py new file mode 100644 index 0000000..0dc7f16 --- /dev/null +++ b/llm_be/chat_backend/migrations/0003_alter_customuser_company.py @@ -0,0 +1,24 @@ +# Generated by Django 3.2.18 on 2024-06-21 19:30 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ("chat_backend", "0002_accouncement"), + ] + + operations = [ + migrations.AlterField( + model_name="customuser", + name="company", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + to="chat_backend.company", + ), + ), + ] diff --git a/llm_be/chat_backend/migrations/0004_rename_accouncement_announcement.py b/llm_be/chat_backend/migrations/0004_rename_accouncement_announcement.py new file mode 100644 index 0000000..afaa21b --- /dev/null +++ b/llm_be/chat_backend/migrations/0004_rename_accouncement_announcement.py @@ -0,0 +1,17 @@ +# Generated by Django 3.2.18 on 2024-06-21 19:32 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ("chat_backend", "0003_alter_customuser_company"), + ] + + operations = [ + migrations.RenameModel( + old_name="Accouncement", + new_name="Announcement", + ), + ] diff --git a/llm_be/chat_backend/migrations/0005_conversation_llmmodels_announcement_created_and_more.py b/llm_be/chat_backend/migrations/0005_conversation_llmmodels_announcement_created_and_more.py new file mode 100644 index 0000000..e1057c7 --- /dev/null +++ b/llm_be/chat_backend/migrations/0005_conversation_llmmodels_announcement_created_and_more.py @@ -0,0 +1,162 @@ +# Generated by Django 4.2.13 on 2024-07-02 15:15 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion +import django.utils.timezone + + +class Migration(migrations.Migration): + + dependencies = [ + ("chat_backend", "0004_rename_accouncement_announcement"), + ] + + operations = [ + migrations.CreateModel( + name="Conversation", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("created", models.DateTimeField(default=django.utils.timezone.now)), + ( + "last_modified", + models.DateTimeField(default=django.utils.timezone.now), + ), + ( + "title", + models.CharField( + default="", + help_text="The title for the conversation", + max_length=64, + ), + ), + ( + "user", + models.OneToOneField( + on_delete=django.db.models.deletion.CASCADE, + to=settings.AUTH_USER_MODEL, + ), + ), + ], + options={ + "abstract": False, + }, + ), + migrations.CreateModel( + name="LLMModels", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("created", models.DateTimeField(default=django.utils.timezone.now)), + ( + "last_modified", + models.DateTimeField(default=django.utils.timezone.now), + ), + ("name", models.CharField(default="", max_length=254)), + ( + "port", + models.IntegerField( + help_text="This specifies the port that the LLM runs on" + ), + ), + ( + "description", + models.CharField( + default="", + help_text="A description for the LLM. Limit is 512 characters", + max_length=512, + ), + ), + ], + options={ + "abstract": False, + }, + ), + migrations.AddField( + model_name="announcement", + name="created", + field=models.DateTimeField(default=django.utils.timezone.now), + ), + migrations.AddField( + model_name="announcement", + name="last_modified", + field=models.DateTimeField(default=django.utils.timezone.now), + ), + migrations.AddField( + model_name="company", + name="created", + field=models.DateTimeField(default=django.utils.timezone.now), + ), + migrations.AddField( + model_name="company", + name="last_modified", + field=models.DateTimeField(default=django.utils.timezone.now), + ), + migrations.CreateModel( + name="Prompt", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("created", models.DateTimeField(default=django.utils.timezone.now)), + ( + "last_modified", + models.DateTimeField(default=django.utils.timezone.now), + ), + ( + "message", + models.CharField( + help_text="The text for a prompt", max_length=10240 + ), + ), + ( + "user_created", + models.BooleanField( + help_text="True if was created by the user. False if it was generate by the LLM" + ), + ), + ( + "conversation", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="chat_backend.conversation", + ), + ), + ], + options={ + "abstract": False, + }, + ), + migrations.AddField( + model_name="company", + name="available_llms", + field=models.ForeignKey( + blank=True, + help_text="A list of LLMs that company can use", + null=True, + on_delete=django.db.models.deletion.CASCADE, + to="chat_backend.llmmodels", + ), + ), + ] diff --git a/llm_be/chat_backend/migrations/0006_customuser_is_company_manager.py b/llm_be/chat_backend/migrations/0006_customuser_is_company_manager.py new file mode 100644 index 0000000..605eba9 --- /dev/null +++ b/llm_be/chat_backend/migrations/0006_customuser_is_company_manager.py @@ -0,0 +1,21 @@ +# Generated by Django 4.2.13 on 2024-07-03 13:26 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("chat_backend", "0005_conversation_llmmodels_announcement_created_and_more"), + ] + + operations = [ + migrations.AddField( + model_name="customuser", + name="is_company_manager", + field=models.BooleanField( + default=False, + help_text="Allows the edit/add/remove of users for a company", + ), + ), + ] diff --git a/llm_be/chat_backend/migrations/0007_alter_prompt_conversation.py b/llm_be/chat_backend/migrations/0007_alter_prompt_conversation.py new file mode 100644 index 0000000..3ba498d --- /dev/null +++ b/llm_be/chat_backend/migrations/0007_alter_prompt_conversation.py @@ -0,0 +1,24 @@ +# Generated by Django 4.2.13 on 2024-07-03 18:33 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ("chat_backend", "0006_customuser_is_company_manager"), + ] + + operations = [ + migrations.AlterField( + model_name="prompt", + name="conversation", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + to="chat_backend.conversation", + ), + ), + ] diff --git a/llm_be/chat_backend/migrations/0008_alter_conversation_user.py b/llm_be/chat_backend/migrations/0008_alter_conversation_user.py new file mode 100644 index 0000000..44b174a --- /dev/null +++ b/llm_be/chat_backend/migrations/0008_alter_conversation_user.py @@ -0,0 +1,25 @@ +# Generated by Django 4.2.13 on 2024-07-03 18:46 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ("chat_backend", "0007_alter_prompt_conversation"), + ] + + operations = [ + migrations.AlterField( + model_name="conversation", + name="user", + field=models.OneToOneField( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + to=settings.AUTH_USER_MODEL, + ), + ), + ] diff --git a/llm_be/chat_backend/migrations/0009_alter_conversation_user.py b/llm_be/chat_backend/migrations/0009_alter_conversation_user.py new file mode 100644 index 0000000..7ba5177 --- /dev/null +++ b/llm_be/chat_backend/migrations/0009_alter_conversation_user.py @@ -0,0 +1,25 @@ +# Generated by Django 4.2.13 on 2024-07-03 18:48 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ("chat_backend", "0008_alter_conversation_user"), + ] + + operations = [ + migrations.AlterField( + model_name="conversation", + name="user", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + to=settings.AUTH_USER_MODEL, + ), + ), + ] diff --git a/llm_be/chat_backend/migrations/0010_conversation_deleted_customuser_deleted.py b/llm_be/chat_backend/migrations/0010_conversation_deleted_customuser_deleted.py new file mode 100644 index 0000000..d8b98f1 --- /dev/null +++ b/llm_be/chat_backend/migrations/0010_conversation_deleted_customuser_deleted.py @@ -0,0 +1,27 @@ +# Generated by Django 4.2.13 on 2024-07-10 14:16 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("chat_backend", "0009_alter_conversation_user"), + ] + + operations = [ + migrations.AddField( + model_name="conversation", + name="deleted", + field=models.BooleanField( + default=False, help_text="This is to hid accounts" + ), + ), + migrations.AddField( + model_name="customuser", + name="deleted", + field=models.BooleanField( + default=False, help_text="This is to hid accounts" + ), + ), + ] diff --git a/llm_be/chat_backend/migrations/0011_customuser_has_signed_tos.py b/llm_be/chat_backend/migrations/0011_customuser_has_signed_tos.py new file mode 100644 index 0000000..c853d37 --- /dev/null +++ b/llm_be/chat_backend/migrations/0011_customuser_has_signed_tos.py @@ -0,0 +1,20 @@ +# Generated by Django 4.2.13 on 2024-09-11 13:22 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("chat_backend", "0010_conversation_deleted_customuser_deleted"), + ] + + operations = [ + migrations.AddField( + model_name="customuser", + name="has_signed_tos", + field=models.BooleanField( + default=False, help_text="If the user has signed the TOS" + ), + ), + ] diff --git a/llm_be/chat_backend/migrations/0012_customuser_slug.py b/llm_be/chat_backend/migrations/0012_customuser_slug.py new file mode 100644 index 0000000..3050eb1 --- /dev/null +++ b/llm_be/chat_backend/migrations/0012_customuser_slug.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.13 on 2024-09-11 18:21 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("chat_backend", "0011_customuser_has_signed_tos"), + ] + + operations = [ + migrations.AddField( + model_name="customuser", + name="slug", + field=models.SlugField(default="luygzxpbkv"), + ), + ] diff --git a/llm_be/chat_backend/migrations/0013_alter_customuser_slug.py b/llm_be/chat_backend/migrations/0013_alter_customuser_slug.py new file mode 100644 index 0000000..21b9936 --- /dev/null +++ b/llm_be/chat_backend/migrations/0013_alter_customuser_slug.py @@ -0,0 +1,19 @@ +# Generated by Django 4.2.13 on 2024-09-11 18:28 + +import autoslug.fields +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ("chat_backend", "0012_customuser_slug"), + ] + + operations = [ + migrations.AlterField( + model_name="customuser", + name="slug", + field=autoslug.fields.AutoSlugField(editable=False, populate_from="email"), + ), + ] diff --git a/llm_be/chat_backend/migrations/0014_feedback.py b/llm_be/chat_backend/migrations/0014_feedback.py new file mode 100644 index 0000000..5076e4f --- /dev/null +++ b/llm_be/chat_backend/migrations/0014_feedback.py @@ -0,0 +1,52 @@ +# Generated by Django 4.2.13 on 2024-09-14 18:29 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ("chat_backend", "0013_alter_customuser_slug"), + ] + + operations = [ + migrations.CreateModel( + name="Feedback", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("text", models.TextField(max_length=512)), + ( + "status", + models.CharField( + choices=[ + ("SUBMITTED", "Submitted"), + ("RESOLVED", "Resolved"), + ("DEFFERED", "Deffered"), + ("CLOSED", "Closed"), + ], + default="SUBMITTED", + max_length=24, + ), + ), + ( + "user", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + to=settings.AUTH_USER_MODEL, + ), + ), + ], + ), + ] diff --git a/llm_be/chat_backend/migrations/0015_feedback_category_feedback_created_and_more.py b/llm_be/chat_backend/migrations/0015_feedback_category_feedback_created_and_more.py new file mode 100644 index 0000000..372c8b9 --- /dev/null +++ b/llm_be/chat_backend/migrations/0015_feedback_category_feedback_created_and_more.py @@ -0,0 +1,42 @@ +# Generated by Django 4.2.13 on 2024-09-15 01:39 + +from django.db import migrations, models +import django.utils.timezone + + +class Migration(migrations.Migration): + + dependencies = [ + ("chat_backend", "0014_feedback"), + ] + + operations = [ + migrations.AddField( + model_name="feedback", + name="category", + field=models.CharField( + choices=[ + ("NOT_DEFINED", "Not defined"), + ("BUG", "Bug"), + ("ENCHANCEMENT", "Enhancment"), + ], + default="NOT_DEFINED", + max_length=24, + ), + ), + migrations.AddField( + model_name="feedback", + name="created", + field=models.DateTimeField(default=django.utils.timezone.now), + ), + migrations.AddField( + model_name="feedback", + name="last_modified", + field=models.DateTimeField(default=django.utils.timezone.now), + ), + migrations.AddField( + model_name="feedback", + name="title", + field=models.TextField(default="", max_length=64), + ), + ] diff --git a/llm_be/chat_backend/migrations/0016_promptmetric_alter_feedback_category.py b/llm_be/chat_backend/migrations/0016_promptmetric_alter_feedback_category.py new file mode 100644 index 0000000..e41acfa --- /dev/null +++ b/llm_be/chat_backend/migrations/0016_promptmetric_alter_feedback_category.py @@ -0,0 +1,90 @@ +# Generated by Django 4.2.13 on 2024-12-30 15:55 + +from django.db import migrations, models +import django.utils.timezone + + +class Migration(migrations.Migration): + + dependencies = [ + ("chat_backend", "0015_feedback_category_feedback_created_and_more"), + ] + + operations = [ + migrations.CreateModel( + name="PromptMetric", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("created", models.DateTimeField(default=django.utils.timezone.now)), + ( + "last_modified", + models.DateTimeField(default=django.utils.timezone.now), + ), + ( + "prompt_id", + models.IntegerField( + help_text="The id of the prompt this matches to" + ), + ), + ( + "event", + models.CharField( + choices=[ + ("CREATED", "Created"), + ("SUBMITTED", "Submitted"), + ("PROCESSED", "Processed"), + ("FINISHED", "Finished"), + ("MAX_PROMPT_METRIC_CHOICES", "Max Prompt Metric Choices"), + ], + default="CREATED", + max_length=26, + ), + ), + ("start_time", models.DateTimeField()), + ("end_time", models.DateTimeField()), + ( + "prompt_length", + models.IntegerField( + help_text="How many characters are in the prompt" + ), + ), + ( + "reponse_legnth", + models.IntegerField( + help_text="How many characters are in the response" + ), + ), + ("has_file", models.BooleanField(help_text="Is there a file")), + ( + "file_type", + models.CharField(help_text="The file type, if any", max_length=16), + ), + ], + options={ + "abstract": False, + }, + ), + migrations.AlterField( + model_name="feedback", + name="category", + field=models.CharField( + choices=[ + ("NOT_DEFINED", "Not defined"), + ("BUG", "Bug"), + ("ENHANCEMENT", "Enhancement"), + ("OTHER", "Other"), + ("MAX_CATEGORIES", "Max Categories"), + ], + default="NOT_DEFINED", + max_length=24, + ), + ), + ] diff --git a/llm_be/chat_backend/migrations/0017_remove_promptmetric_reponse_legnth_and_more.py b/llm_be/chat_backend/migrations/0017_remove_promptmetric_reponse_legnth_and_more.py new file mode 100644 index 0000000..61535a1 --- /dev/null +++ b/llm_be/chat_backend/migrations/0017_remove_promptmetric_reponse_legnth_and_more.py @@ -0,0 +1,54 @@ +# Generated by Django 4.2.13 on 2024-12-30 20:55 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("chat_backend", "0016_promptmetric_alter_feedback_category"), + ] + + operations = [ + migrations.RemoveField( + model_name="promptmetric", + name="reponse_legnth", + ), + migrations.AddField( + model_name="promptmetric", + name="conversation_id", + field=models.IntegerField( + default=0, help_text="The id of the conversation this matches to" + ), + preserve_default=False, + ), + migrations.AddField( + model_name="promptmetric", + name="model_name", + field=models.CharField( + default="temp", help_text="The name of the model", max_length=215 + ), + preserve_default=False, + ), + migrations.AddField( + model_name="promptmetric", + name="reponse_length", + field=models.IntegerField( + blank=True, + help_text="How many characters are in the response", + null=True, + ), + ), + migrations.AlterField( + model_name="promptmetric", + name="end_time", + field=models.DateTimeField(blank=True, null=True), + ), + migrations.AlterField( + model_name="promptmetric", + name="file_type", + field=models.CharField( + blank=True, help_text="The file type, if any", max_length=16, null=True + ), + ), + ] diff --git a/llm_be/chat_backend/migrations/0018_prompt_file_prompt_file_type.py b/llm_be/chat_backend/migrations/0018_prompt_file_prompt_file_type.py new file mode 100644 index 0000000..54aec2a --- /dev/null +++ b/llm_be/chat_backend/migrations/0018_prompt_file_prompt_file_type.py @@ -0,0 +1,36 @@ +# Generated by Django 4.2.17 on 2024-12-31 16:01 + +import django.core.files.storage +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("chat_backend", "0017_remove_promptmetric_reponse_legnth_and_more"), + ] + + operations = [ + migrations.AddField( + model_name="prompt", + name="file", + field=models.FileField( + blank=True, + help_text="file for the prompt", + null=True, + upload_to=django.core.files.storage.FileSystemStorage( + location="prompt_files" + ), + ), + ), + migrations.AddField( + model_name="prompt", + name="file_type", + field=models.CharField( + blank=True, + help_text="file type of the file for the prompt", + max_length=16, + null=True, + ), + ), + ] diff --git a/llm_be/chat_backend/migrations/0019_customuser_conversation_order_and_more.py b/llm_be/chat_backend/migrations/0019_customuser_conversation_order_and_more.py new file mode 100644 index 0000000..628e4ee --- /dev/null +++ b/llm_be/chat_backend/migrations/0019_customuser_conversation_order_and_more.py @@ -0,0 +1,27 @@ +# Generated by Django 4.2.17 on 2025-02-17 15:24 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("chat_backend", "0018_prompt_file_prompt_file_type"), + ] + + operations = [ + migrations.AddField( + model_name="customuser", + name="conversation_order", + field=models.BooleanField( + default=True, help_text="How the conversations should display" + ), + ), + migrations.AlterField( + model_name="conversation", + name="deleted", + field=models.BooleanField( + default=False, help_text="This is to hide conversations" + ), + ), + ] diff --git a/llm_be/chat_backend/migrations/__init__.py b/llm_be/chat_backend/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/llm_be/chat_backend/models.py b/llm_be/chat_backend/models.py new file mode 100644 index 0000000..7505470 --- /dev/null +++ b/llm_be/chat_backend/models.py @@ -0,0 +1,193 @@ +from django.db import models +from django.contrib.auth.models import AbstractUser +from django.utils import timezone +from autoslug import AutoSlugField +from django.core.files.storage import FileSystemStorage +# Create your models here. + +FILE_STORAGE = FileSystemStorage(location='prompt_files') + +class TimeInfoBase(models.Model): + + created = models.DateTimeField(default=timezone.now) + last_modified = models.DateTimeField(default=timezone.now) + + class Meta: + abstract = True + + def save(self, *args, **kwargs): + if not kwargs.pop("skip_last_modified", False) and not hasattr( + self, "skip_last_modified" + ): + self.last_modified = timezone.now() + if kwargs.get("update_fields") is not None: + kwargs["update_fields"] = list( + {*kwargs["update_fields"], "last_modified"} + ) + + super().save(*args, **kwargs) + + +class LLMModels(TimeInfoBase): + name = models.CharField(max_length=254, default="") + port = models.IntegerField(help_text="This specifies the port that the LLM runs on") + description = models.CharField( + max_length=512, + default="", + help_text="A description for the LLM. Limit is 512 characters", + ) + + +class Company(TimeInfoBase): + name = models.TextField(max_length=256) + state = models.TextField(max_length=2) + zipcode = models.TextField(max_length=5) + address = models.TextField(max_length=256) + available_llms = models.ForeignKey( + "LLMModels", + on_delete=models.CASCADE, + blank=True, + null=True, + help_text="A list of LLMs that company can use", + ) + + +class CustomUser(AbstractUser): + company = models.ForeignKey( + Company, on_delete=models.CASCADE, blank=True, null=True + ) + is_company_manager = models.BooleanField( + help_text="Allows the edit/add/remove of users for a company", default=False + ) + deleted = models.BooleanField(help_text="This is to hid accounts", default=False) + has_signed_tos = models.BooleanField(default=False, help_text="If the user has signed the TOS") + slug = AutoSlugField(populate_from='email') + conversation_order = models.BooleanField(default=True, help_text='How the conversations should display') + def get_set_password_url(self): + return f"https://www.chat.aimloperations.com/set_password?slug={self.slug}" + +FEEDBACK_CHOICE = ( + ("SUBMITTED", "Submitted"), + ("RESOLVED", "Resolved"), + ("DEFFERED", "Deffered"), + ("CLOSED", "Closed"), +) + +FEEDBACK_CATEGORIES = ( + ('NOT_DEFINED', 'Not defined'), + ('BUG', 'Bug'), + ('ENHANCEMENT', 'Enhancement'), + ('OTHER', 'Other'), + ('MAX_CATEGORIES', 'Max Categories'), +) + +class Feedback(TimeInfoBase): + title = models.TextField(max_length=64, default='') + user = models.ForeignKey( + CustomUser, on_delete=models.CASCADE, blank=True, null=True + ) + text = models.TextField(max_length=512) + status = models.CharField(max_length=24, choices=FEEDBACK_CHOICE, default="SUBMITTED") + category = models.CharField(max_length=24, choices=FEEDBACK_CATEGORIES, default="NOT_DEFINED") + + def get_user_email(self): + if self.user: + return self.user.email + else: + return "" + + +MONTH_CHOICES = ( + ("JANUARY", "January"), + ("FEBRUARY", "February"), + ("MARCH", "March"), + # .... + ("DECEMBER", "December"), +) + +month = models.CharField(max_length=9, + choices=MONTH_CHOICES, + default="JANUARY") + +class Announcement(TimeInfoBase): + class Status(models.TextChoices): + default = "DEFAULT", "default" + warning = "WARNING", "warning" + info = "INFO", "info" + danger = "DANGER", "danger" + + status = models.CharField( + max_length=7, choices=Status.choices, default=Status.default + ) + message = models.TextField(max_length=256) + start_date_time = models.DateTimeField(auto_now=True) + end_date_time = models.DateTimeField(auto_now=True) + + +class Conversation(TimeInfoBase): + user = models.ForeignKey( + CustomUser, on_delete=models.CASCADE, blank=True, null=True + ) + title = models.CharField( + max_length=64, help_text="The title for the conversation", default="" + ) + deleted = models.BooleanField(help_text="This is to hide conversations", default=False) + + def get_user_email(self): + if self.user: + return self.user.email + else: + return "" + + def __str__(self): + return self.title + + +class Prompt(TimeInfoBase): + message = models.CharField(max_length=10 * 1024, help_text="The text for a prompt") + user_created = models.BooleanField( + help_text="True if was created by the user. False if it was generate by the LLM" + ) + conversation = models.ForeignKey( + "Conversation", on_delete=models.CASCADE, blank=True, null=True + ) + file =models.FileField(upload_to=FILE_STORAGE, blank=True, null=True, help_text="file for the prompt") + file_type=models.CharField(max_length=16, blank=True, null=True, help_text='file type of the file for the prompt') + + def get_conversation_title(self): + if self.conversation: + return self.conversation.title + else: + return "" + + def file_exists(self): + return self.file != None and self.file.storage.exists(self.file.name) + + + +class PromptMetric(TimeInfoBase): + PROMPT_METRIC_CHOICES = ( + ("CREATED", "Created"), + ("SUBMITTED", "Submitted"), + ("PROCESSED", "Processed"), + ("FINISHED", "Finished"), + ("MAX_PROMPT_METRIC_CHOICES", "Max Prompt Metric Choices"), + ) + prompt_id = models.IntegerField(help_text="The id of the prompt this matches to") + conversation_id = models.IntegerField(help_text="The id of the conversation this matches to") + event = models.CharField( + max_length=26, choices=PROMPT_METRIC_CHOICES, default='CREATED' + ) + model_name = models.CharField(max_length=215, help_text="The name of the model") + start_time = models.DateTimeField() + end_time = models.DateTimeField(blank=True, null=True) + prompt_length = models.IntegerField( help_text="How many characters are in the prompt") + reponse_length = models.IntegerField(blank=True, null=True, help_text="How many characters are in the response") + has_file = models.BooleanField(help_text="Is there a file") + file_type = models.CharField(max_length=16, help_text='The file type, if any', blank=True, null=True) + + def get_duration(self): + if(self.start_time and self.end_time): + difference =self.end_time - self.start_time + return difference.seconds + return 0 diff --git a/llm_be/chat_backend/renderers.py b/llm_be/chat_backend/renderers.py new file mode 100644 index 0000000..f611bb0 --- /dev/null +++ b/llm_be/chat_backend/renderers.py @@ -0,0 +1,8 @@ +from rest_framework.renderers import BaseRenderer + +class ServerSentEventRenderer(BaseRenderer): + media_type = 'text/event-stream' + format = 'txt' + + def render(self, data, accepted_media_type=None, renderer_context=None): + return data \ No newline at end of file diff --git a/llm_be/chat_backend/routing.py b/llm_be/chat_backend/routing.py new file mode 100644 index 0000000..70829b5 --- /dev/null +++ b/llm_be/chat_backend/routing.py @@ -0,0 +1,7 @@ +from django.urls import re_path +from .views import ChatConsumerAgain + +websocket_urlpatterns = [ + re_path(r'ws/chat_again/$', ChatConsumerAgain.as_asgi()), + +] \ No newline at end of file diff --git a/llm_be/chat_backend/serializers.py b/llm_be/chat_backend/serializers.py new file mode 100644 index 0000000..07c0dcc --- /dev/null +++ b/llm_be/chat_backend/serializers.py @@ -0,0 +1,69 @@ +from rest_framework_simplejwt.serializers import TokenObtainPairSerializer +from rest_framework import serializers +from .models import CustomUser, Announcement, Company, Conversation, Prompt, Feedback, FEEDBACK_CATEGORIES + + +class MyTokenObtainPairSerializer(TokenObtainPairSerializer): + @classmethod + def get_token(cls, user): + token = super(MyTokenObtainPairSerializer, cls).get_token(user) + + # add custom claim + token["company"] = "something here" + + return token + + +class CompanySerializer(serializers.ModelSerializer): + class Meta: + model = Company + fields = "__all__" + + +class AnnouncmentSerializer(serializers.ModelSerializer): + class Meta: + model = Announcement + fields = "__all__" + +class FeedbackSerializer(serializers.ModelSerializer): + class Meta: + model = Feedback + fields = "__all__" + +class CustomUserSerializer(serializers.ModelSerializer): + email = serializers.EmailField(required=True) + username = serializers.CharField() + password = serializers.CharField(min_length=8, write_only=True) + company = CompanySerializer() + has_usable_password = serializers.BooleanField() + + class Meta: + model = CustomUser + fields = "__all__" + extra_kwargs = {"password": {"write_only": True}} + + # def create(self, validated_data): + # password = validated_data.pop('password',None) + # instance = self.Meta.model(**validated_data) + # if password is not None: + # instance.set_password(password) + # instance.save() + # return instance + + +class ConversationSerializer(serializers.ModelSerializer): + class Meta: + model = Conversation + fields = ("title", "created", "last_modified", "id") + + +class PromptSerializer(serializers.ModelSerializer): + + class Meta: + model = Prompt + fields = ("message", "user_created", "created", "id", ) + +class BasicUserSerializer(serializers.ModelSerializer): + class Meta: + model = CustomUser + fields = ("email", "first_name", "last_name", "is_active","has_usable_password","is_company_manager",'has_signed_tos') \ No newline at end of file diff --git a/llm_be/chat_backend/templates/emails/feedback_email.html b/llm_be/chat_backend/templates/emails/feedback_email.html new file mode 100644 index 0000000..3dd5f4a --- /dev/null +++ b/llm_be/chat_backend/templates/emails/feedback_email.html @@ -0,0 +1,105 @@ + + + + + + New Feedback Submission + + + + + + + +
+ + +
+ + \ No newline at end of file diff --git a/llm_be/chat_backend/templates/emails/feedback_email.txt b/llm_be/chat_backend/templates/emails/feedback_email.txt new file mode 100644 index 0000000..1c5c6bb --- /dev/null +++ b/llm_be/chat_backend/templates/emails/feedback_email.txt @@ -0,0 +1,3 @@ +New feedback for Chat by AI ML Operations, LLC + +"New Feedback. {{ title }}. {{ feedback_text }}" \ No newline at end of file diff --git a/llm_be/chat_backend/templates/emails/invite_email.html b/llm_be/chat_backend/templates/emails/invite_email.html new file mode 100644 index 0000000..b358c97 --- /dev/null +++ b/llm_be/chat_backend/templates/emails/invite_email.html @@ -0,0 +1,97 @@ + + + + + + Invitation to Chat by AI ML Operations, LLC + + + + + + + +
+ + +
+ + \ No newline at end of file diff --git a/llm_be/chat_backend/templates/emails/invite_email.txt b/llm_be/chat_backend/templates/emails/invite_email.txt new file mode 100644 index 0000000..0ba347b --- /dev/null +++ b/llm_be/chat_backend/templates/emails/invite_email.txt @@ -0,0 +1,3 @@ +Welcome to AI ML Operations, LLC Chat Services + +"Welcome to chat.aimloperations.com. Please use {{ url }} to set your password" \ No newline at end of file diff --git a/llm_be/chat_backend/tests.py b/llm_be/chat_backend/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/llm_be/chat_backend/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/llm_be/chat_backend/urls.py b/llm_be/chat_backend/urls.py new file mode 100644 index 0000000..d32c9c5 --- /dev/null +++ b/llm_be/chat_backend/urls.py @@ -0,0 +1,52 @@ +from django.urls import path +from rest_framework_simplejwt import views as jwt_views +from .views import ( + AcknowledgeTermsOfService, + CustomObtainTokenView, + CustomUserCreate, + CustomUserInvite, + LogoutAndBlacklistRefreshTokenForUserView, + CustomUserGet, + is_authenticated, + AnnouncmentView, + FeedbackView, + ConversationsView, + ConversationDetailView, + CompanyUsersView, + SetUserPassword, + ConversationPreferences, + UserPromptAnalytics, + UserConversationAnalytics, + CompanyUsageAnalytics, + AdminAnalytics +) + +urlpatterns = [ + path("token/obtain/", CustomObtainTokenView.as_view(), name="token_create"), + path("token/refresh/", jwt_views.TokenRefreshView.as_view(), name="token_refresh"), + path("user/create/", CustomUserCreate.as_view(), name="create_user"), + path("user/invite/", CustomUserInvite.as_view(), name="invite_user"), + path("user/set_password//", SetUserPassword.as_view(), name="set_password"), + path( + "blacklist/", + LogoutAndBlacklistRefreshTokenForUserView.as_view(), + name="blacklist", + ), + path("user/get/", CustomUserGet.as_view(), name="get_user"), + path("user/acknowledge_tos/", AcknowledgeTermsOfService.as_view(), name="acknowledge_tos"), + path("company_users",CompanyUsersView.as_view(), name="company_users"), + path("user/is_authenticated/", is_authenticated, name="is_authenticated"), + path("announcment/get/", AnnouncmentView.as_view(), name="get_announcments"), + path("conversations", ConversationsView.as_view(), name="conversations"), + path("feedbacks/", FeedbackView.as_view(), name="feedbacks"), + path( + "conversation_details", + ConversationDetailView.as_view(), + name="conversation_details", + ), + path("conversation_preferences", ConversationPreferences.as_view(), name="conversation_preferences"), + path("analytics/user_prompts/", UserPromptAnalytics.as_view(), name="analytics_user_prompts"), + path("analytics/user_conversations/", UserConversationAnalytics.as_view(), name="analytics_user_conversations"), + path("analytics/company_usage/", CompanyUsageAnalytics.as_view(), name="analytics_company_usage"), + path("analytics/admin/", AdminAnalytics.as_view(), name="analytics_admin"), +] diff --git a/llm_be/chat_backend/utils.py b/llm_be/chat_backend/utils.py new file mode 100644 index 0000000..ba6a9f6 --- /dev/null +++ b/llm_be/chat_backend/utils.py @@ -0,0 +1,7 @@ +import datetime + +def last_day_of_month(any_day): + # The day 28 exists in every month. 4 days later, it's always next month + next_month = any_day.replace(day=28) + datetime.timedelta(days=4) + # subtracting the number of the current day brings us back one month + return next_month - datetime.timedelta(days=next_month.day) \ No newline at end of file diff --git a/llm_be/chat_backend/views.py b/llm_be/chat_backend/views.py new file mode 100644 index 0000000..7a87583 --- /dev/null +++ b/llm_be/chat_backend/views.py @@ -0,0 +1,714 @@ +from channels.layers import get_channel_layer +from channels.db import database_sync_to_async +from rest_framework_simplejwt.views import TokenObtainPairView +from rest_framework_simplejwt.tokens import RefreshToken +from rest_framework import permissions, status +from .serializers import ( + MyTokenObtainPairSerializer, + CustomUserSerializer, + BasicUserSerializer, + AnnouncmentSerializer, + CompanySerializer, + ConversationSerializer, + PromptSerializer, + FeedbackSerializer +) +from rest_framework.views import APIView +from rest_framework.response import Response +from .models import CustomUser, Announcement, Conversation, Prompt, Feedback,PromptMetric +from django.views.decorators.cache import never_cache +from django.http import JsonResponse +from datetime import datetime +from .client import LlamaClient +from asgiref.sync import sync_to_async, async_to_sync +from channels.generic.websocket import AsyncWebsocketConsumer +from langchain_ollama.llms import OllamaLLM +from langchain_core.prompts import ChatPromptTemplate +from langchain_core.messages import HumanMessage, SystemMessage +import re +from django.conf import settings +import json +import base64 +import pandas as pd + +# For email support +from django.core.mail import EmailMultiAlternatives +from django.template.loader import render_to_string +from django.utils.html import strip_tags +from django.template.loader import get_template +from django.template import Context +from django.utils import timezone +from django.core.files import File +from django.core.files.base import ContentFile +import math +import datetime +import pytz + +from dateutil.relativedelta import relativedelta + +from .utils import last_day_of_month + + +CHANNEL_NAME: str = 'llm_messages' +MODEL_NAME: str = "llama3" + +# Create your views here. +class CustomObtainTokenView(TokenObtainPairView): + permission_classes = (permissions.AllowAny,) + serializer_class = MyTokenObtainPairSerializer + + +class CustomUserCreate(APIView): + permission_classes = (permissions.AllowAny,) + authentication_classes = () + + def post(self, request, format="json"): + serializer = CustomUserSerializer(data=request.data) + if serializer.is_valid(): + user = serializer.save() + if user: + json = serializer.data + return Response(json, status=status.HTTP_201_CREATED) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + +def send_invite_email(slug, email_to_invite): + print(f"url : https://www.chat.aimloperations.com/set_password?slug={slug}") + url = f"https://www.chat.aimloperations.com/set_password?slug={slug}" + subject = "Welcome to AI ML Operations, LLC Chat Services" + from_email = "ryan@aimloperations.com" + to=email_to_invite + d = {"url": url} + html_content = get_template(r'emails/invite_email.html').render(d) + text_content = get_template(r'emails/invite_email.txt').render(d) + + msg = EmailMultiAlternatives(subject, text_content, from_email, [to]) + msg.attach_alternative(html_content, "text/html") + msg.send(fail_silently=True) + +def send_feedback_email(feedback_obj): + subject = "New Feedback for Chat by AI ML Operations, LLC" + from_email = "ryan@aimloperations.com" + to="ryan@aimloperations.com" + d = {"title": feedback_obj.title, "feedback_text": feedback_obj.text} + html_content = get_template(r'emails/feedback_email.html').render(d) + text_content = get_template(r'emails/feedback_email.txt').render(d) + + msg = EmailMultiAlternatives(subject, text_content, from_email, [to]) + msg.attach_alternative(html_content, "text/html") + msg.send(fail_silently=True) + +class CustomUserInvite(APIView): + http_method_names = ['post'] + def post(self, request, format="json"): + def valid_email(email_string): + regex = r'^[a-z0-9]+[\._]?[a-z0-9]+[@]\w+[.]\w+$' + if re.match(regex,email_string): + return True + else: + return False + + email_to_invite = request.data['email'] + + if len(email_to_invite) == 0 or not valid_email(email_to_invite) or not request.user.is_company_manager: + return Response(status=status.HTTP_400_BAD_REQUEST) + # make sure there isn't a user with this email already + existing_users = CustomUser.objects.filter(email=email_to_invite) + if len(existing_users) > 0: + return Response(status=status.HTTP_400_BAD_REQUEST) + # create the object and send the email + user = CustomUser.objects.create(email=email_to_invite, username=email_to_invite, company=request.user.company) + + # send an email + send_invite_email(user.slug, email_to_invite) + + + + return Response(status=status.HTTP_201_CREATED) + +class SetUserPassword(APIView): + http_method_names = ['post','get'] + permission_classes = (permissions.AllowAny,) + authentication_classes = () + def get(self, request, slug): + user = CustomUser.objects.get(slug=slug) + if user.last_login: + return Response(status=status.HTTP_401_UNAUTHORIZED) + else: + return Response(status=status.HTTP_200_OK) + def post(self, request, slug, format="json"): + user = CustomUser.objects.get(slug=slug) + user.set_password(request.data['password']) + user.save() + return Response(status=status.HTTP_200_OK) + + + +class CustomUserGet(APIView): + http_method_names = ['get', 'head', 'post'] + def get(self, request, format="json"): + + email = request.user.email + username = request.user.username + user = CustomUser.objects.get(email=email) + serializer = CustomUserSerializer(user) + + return Response(serializer.data, status=status.HTTP_200_OK) + +class FeedbackView(APIView): + http_method_names = ['post','get'] + def post(self, request, format="json"): + serializer = FeedbackSerializer(data=request.data) + print(request.data) + if serializer.is_valid(): + + feedback_obj = serializer.save() + feedback_obj.user = request.user + + feedback_obj.save() + send_feedback_email(feedback_obj) + return Response(serializer.data, status=status.HTTP_201_CREATED) + else: + print(serializer.errors) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + def get(self, request, format="json"): + feedback_objs = Feedback.objects.filter(user=request.user) + serializer = FeedbackSerializer(feedback_objs, many=True) + return Response(serializer.data, status=status.HTTP_200_OK) + + + +class AcknowledgeTermsOfService(APIView): + http_method_names = ['post'] + def post(self, request, format="json"): + request.user.has_signed_tos = True + request.user.save() + return Response(status=status.HTTP_200_OK) + +class CompanyUsersView(APIView): + def get(self, request, format="json"): + # TODO: make sure you are a manager of that company + if request.user.is_company_manager: + users = CustomUser.objects.filter(company_id=request.user.company.id) + serializer = BasicUserSerializer(users, many=True) + return Response(serializer.data, status=status.HTTP_200_OK) + else: + return Response(status=status.HTTP_401_UNAUTHORIZED) + + + def post(self, request, format="json"): + if request.user.is_company_manager: + user = CustomUser.objects.get(email=request.data.get("email")) + if request.user.company_id == user.company_id: + field = request.data.get("field") + data = {} + if field == "is_active": + data.update({"is_active": not user.is_active}) + elif field == "company_manager": + data.update({"is_company_manager": not user.is_company_manager}) + elif field == "has_password": + if user.has_usable_password(): + user.set_unusable_password() + serializer = CustomUserSerializer(user, data, partial=True) + if serializer.is_valid(): + serializer.save() + return Response(status=status.HTTP_200_OK) + return Response(status=status.HTTP_400_BAD_REQUEST) + return Response(status=status.HTTP_401_UNAUTHORIZED) + + def delete(self, request, format="json"): + if request.user.is_company_manager: + user = CustomUser.objects.get(email=request.data.get("email")) + if request.user.company_id == user.company_id: + user.delete() + return Response(status=status.HTTP_200_OK) + return Response(status=status.HTTP_401_UNAUTHORIZED) + +class AnnouncmentView(APIView): + permission_classes = (permissions.AllowAny,) + serializer_class = AnnouncmentSerializer + + def get(self, request, format="json"): + announcements = Announcement.objects.all() + serializer = AnnouncmentSerializer(announcements, many=True) + + return Response(serializer.data, status=status.HTTP_200_OK) + + +class LogoutAndBlacklistRefreshTokenForUserView(APIView): + permission_classes = (permissions.AllowAny,) + authentication_classes = () + + def post(self, request): + try: + refresh_token = request.data["refresh_token"] + token = RefreshToken(refresh_token) + token.blacklist() + return Response(status=status.HTTP_205_RESET_CONTENT) + except Exception as e: + return Response(status=status.HTTP_400_BAD_REQUEST) + + +@never_cache +def is_authenticated(request): + if request.user.is_authenticated: + return JsonResponse({}, status=status.HTTP_200_OK) + return JsonResponse({}, status=status.HTTP_401_UNAUTHORIZED) + + +class ConversationsView(APIView): + def get(self, request, format="json"): + order = "created" if request.user.conversation_order else "-created" + conversations = Conversation.objects.filter(user=request.user, deleted=False).order_by(order) + serializer = ConversationSerializer(conversations, many=True) + return Response(serializer.data, status=status.HTTP_200_OK) + + def post(self, request, format="json"): + """ + Create a blank conversation and return the title and id number + """ + title = request.data.get("name") + conversation = Conversation.objects.create(title=title) + conversation.save() + conversation.user_id = request.user.id + conversation.save() + + # TODO: when we are smart enough to create a conversation when a prompt is sent + # client = LlamaClient() + # inital_message = request.data.get("prompt") + # title = client.generate_conversation_title(inital_message) + # title = title if title else "New Conversation" + # conversation = Conversation.objects.create(title=title) + # conversation.save() + # conversation.user_id = request.user.id + # conversation.save() + + return Response({"title": title, "id": conversation.id}, status=status.HTTP_201_CREATED) + + +class ConversationPreferences(APIView): + def get(self, request, format="json"): + user = request.user + return Response({"order": user.conversation_order}, status=status.HTTP_200_OK) + + def post(self, request, format="json"): + user = request.user + user.conversation_order = not user.conversation_order + user.save() + return Response({"order": user.conversation_order}, status=status.HTTP_200_OK) + + + +class ConversationDetailView(APIView): + def get(self, request, format="json"): + conversation_id = request.query_params.get("conversation_id") + prompts = Prompt.objects.filter(conversation__id=conversation_id) + serailzer = PromptSerializer(prompts, many=True) + return Response(serailzer.data, status=status.HTTP_200_OK) + + + def post(self, request, format="json"): + print('In the post') + # Add the prompt to the database + # make sure there is a conversation for it + # if there is not a conversation create a title for it + + # make sure that our model exists and it is running + prompt = request.data.get("prompt") + + conversation_id = request.data.get("conversation_id") + is_user = bool(request.data.get("is_user")) + + try: + conversation = Conversation.objects.get(id=conversation_id) + + # add the prompt to the conversation + serializer = PromptSerializer( + data={ + "message": prompt, + "user_created": is_user, + "created": datetime.now(), + } + ) + if serializer.is_valid(): + prompt_instance = serializer.save() + prompt_instance.conversation_id = conversation.id + prompt_instance = serializer.save() + + # set up the streaming response if it is from the user + print(f'Do we have a valid user? {is_user}') + if is_user: + messages = [] + for prompt_obj in Prompt.objects.filter(conversation__id=conversation_id): + messages.append({ + 'content':prompt_obj.message, + 'role': 'user' if prompt_obj.user_created else 'assistant' + }) + + channel_layer = get_channel_layer() + print(f'Sending to the channel: {CHANNEL_NAME}') + async_to_sync(channel_layer.group_send)( + CHANNEL_NAME, { + 'type':'receive', + 'content': messages + } + ) + except: + print(f"Error trying to submit to conversation_id: {conversation_id} with request.data: {request.data}") + pass + + + return Response(status=status.HTTP_200_OK) + + def delete(self, request, format="json"): + conversation_id = request.data.get("conversation_id") + conversation = Conversation.objects.get(id=conversation_id, user=request.user) + conversation.deleted = True + conversation.save() + return Response(status=status.HTTP_202_ACCEPTED) + +class UserPromptAnalytics(APIView): + def get(self, request, format="json"): + now = timezone.now() + result = [] + + number_of_months = 3 + company_user_ids = CustomUser.objects.filter(company=request.user.company).values_list('id', flat=True) + for i in range(number_of_months): + next_year = now.year + next_month = now.month - i + while next_month < 1: + next_year -= 1 + next_month += 12 + + start_date = datetime.datetime(next_year, next_month, 1) + end_date = last_day_of_month(start_date) + total_conversations = Conversation.objects.filter(created__gte=start_date, created__lte=end_date) + total_prompts = Prompt.objects.filter(conversation__id__in=total_conversations, created__gte=start_date, created__lte=end_date) + total_users = len(CustomUser.objects.all()) + my_conversations = Conversation.objects.filter(user=request.user) + my_prompts = Prompt.objects.filter(conversation__in=my_conversations, created__gte=start_date, created__lte=end_date) + company_conversations = Conversation.objects.filter(user__id__in=company_user_ids) + company_prompts = Prompt.objects.filter(conversation__in=company_conversations, created__gte=start_date, created__lte=end_date) + + result.append({ + "month":start_date.strftime("%B"), + "you": len(my_prompts), + "others": len(company_prompts)/len(company_user_ids), + "all":len(total_prompts)/total_users + }) + + return Response(result[::-1], status=status.HTTP_200_OK) + +class UserConversationAnalytics(APIView): + def get(self, request, format="json"): + now = timezone.now() + result = [] + + number_of_months = 3 + company_user_ids = CustomUser.objects.filter(company=request.user.company).values_list('id', flat=True) + for i in range(number_of_months): + next_year = now.year + next_month = now.month - i + while next_month < 1: + next_year -= 1 + next_month += 12 + + start_date = datetime.datetime(next_year, next_month, 1) + end_date = last_day_of_month(start_date) + total_conversations = len(Conversation.objects.filter(created__gte=start_date, created__lte=end_date)) + total_users = len(CustomUser.objects.all()) + company_conversations = len(Conversation.objects.filter(user__id__in=company_user_ids, created__gte=start_date, created__lte=end_date)) + + result.append({ + "month":start_date.strftime("%B"), + "you": len(Conversation.objects.filter(user=request.user, created__gte=start_date, created__lte=end_date)), + "others": company_conversations/len(company_user_ids), + "all":total_conversations/total_users + }) + + + return Response(result[::-1], status=status.HTTP_200_OK) + +class CompanyUsageAnalytics(APIView): + def get(self, request, format="json"): + now = timezone.now() + result = [] + + number_of_months = 3 + company_user_ids = CustomUser.objects.filter(company=request.user.company).values_list('id', flat=True) + + for i in range(number_of_months): + next_year = now.year + next_month = now.month - i + while next_month < 1: + next_year -= 1 + next_month += 12 + + start_date = datetime.datetime(next_year, next_month, 1) + end_date = last_day_of_month(start_date) + conversations = Conversation.objects.filter(user__id__in=company_user_ids, created__gte=start_date, created__lte=end_date) + + conversation_user_ids = conversations.values_list("user__id", flat=True).distinct() + result.append({ + "month":start_date.strftime("%B"), + "used":len(conversation_user_ids), + "not_used":len(company_user_ids) - len(conversation_user_ids) + }) + return Response(result[::-1], status=status.HTTP_200_OK) + +class AdminAnalytics(APIView): + def get(self, request, format="json"): + number_of_months = 3 + result = [] + now = timezone.now() + + for i in range(number_of_months): + next_year = now.year + next_month = now.month - i + while next_month < 1: + next_year -= 1 + next_month += 12 + + start_date = datetime.datetime(next_year, next_month, 1) + end_date = last_day_of_month(start_date) + durations = [item.get_duration() for item in PromptMetric.objects.filter(created__gte=start_date, created__lte=end_date)] + if len(durations) == 0: + result.append({ + "month":start_date.strftime("%B"), + "range":[0,0], + "avg": 0, + "median":0, + }) + continue + + average = sum(durations)/len(durations) + min_value = min(durations) + max_value = max(durations) + durations.sort() + median = durations[len(durations)//2] + result.append({ + "month":start_date.strftime("%B"), + "range":[min_value,max_value], + "avg": average, + "median":median, + }) + + + + + return Response(result[::-1], status=status.HTTP_200_OK) + +prompt = ChatPromptTemplate.from_messages([ + ("system", "You are a helpful assistant."), + ("user", "{input}") +]) + +llm = OllamaLLM(model=MODEL_NAME) + +# output_parser = StrOutputParser() +# # Chain +# chain = prompt | llm.with_config({"run_name": "model"}) | output_parser.with_config({"run_name": "Assistant"}) + +@database_sync_to_async +def create_conversation(prompt, email): + # return the conversation id + + + response = llm.invoke("Summarise the phrase in one to for words\"%s\"" % prompt) + print(f"Response: {response}") + print(dir(response)) + title = response.replace("\"","") + title = " ".join(title.split(" ")[:4]) + + + conversation = Conversation.objects.create(title = title) + conversation.save() + + user = CustomUser.objects.get(email=email) + conversation.user_id = user.id + conversation.save() + return conversation.id + +@database_sync_to_async +def get_messages(conversation_id, prompt, file_string: str = None, file_type: str = ""): + messages = [] + + conversation = Conversation.objects.get(id=conversation_id) + print(file_string) + + # add the prompt to the conversation + serializer = PromptSerializer( + data={ + "message": prompt, + "user_created": True, + "created": datetime.now(), + } + ) + if serializer.is_valid(raise_exception=True): + prompt_instance = serializer.save() + prompt_instance.conversation_id = conversation.id + prompt_instance.save() + if file_string: + file_name = f"prompt_{prompt_instance.id}_data.{file_type}" + f = ContentFile(file_string, name=file_name) + prompt_instance.file.save(file_name, f) + prompt_instance.file_type = file_type + prompt_instance.save() + + + + for prompt_obj in Prompt.objects.filter(conversation__id=conversation_id): + messages.append({ + 'content': prompt_obj.message, + 'role': 'user' if prompt_obj.user_created else 'assistant', + 'has_file': prompt_obj.file_exists(), + 'file': prompt_obj.file if prompt_obj.file_exists() else None, + 'file_type': prompt_obj.file_type if prompt_obj.file_exists() else None, + }) + + # now transform the messages + transformed_messages = [] + for message in messages: + + if message['has_file'] and message['file_type'] != None: + if 'csv' in message['file_type']: + file_type = 'csv' + altered_message = f"{message['content']}\n The file type is csv and the file contents are: {message['file'].read()}" + elif 'xlsx' in message['file_type']: + file_type = 'xlsx' + df = pd.read_excel(message['file'].read()) + altered_message = f"{message['content']}\n The file type is xlsx and the file contents are: {df}" + elif 'txt' in message['file_type']: + file_type = 'txt' + altered_message = f"{message['content']}\n The file type is csv and the file contents are: {message['file'].read()}" + else: + altered_message = message['content'] + + transformed_message = SystemMessage(content=altered_message) if message['role'] == 'assistant' else HumanMessage(content=altered_message) + transformed_messages.append(transformed_message) + + return transformed_messages, prompt_instance + + +@database_sync_to_async +def save_generated_message(conversation_id, message): + conversation = Conversation.objects.get(id=conversation_id) + + # add the prompt to the conversation + serializer = PromptSerializer( + data={ + "message": message, + "user_created": False, + "created": datetime.now(), + } + ) + if serializer.is_valid(): + prompt_instance = serializer.save() + prompt_instance.conversation_id = conversation.id + prompt_instance = serializer.save() + +@database_sync_to_async +def create_prompt_metric(prompt_id, prompt, has_file, file_type, model_name, conversation_id): + prompt_metric = PromptMetric.objects.create( + prompt_id=prompt_id, + start_time = timezone.now(), + prompt_length = len(prompt), + has_file = has_file, + file_type = file_type, + model_name=model_name, + conversation_id=conversation_id, + ) + prompt_metric.save() + return prompt_metric + +@database_sync_to_async +def update_prompt_metric(prompt_metric, status): + prompt_metric.event = status + prompt_metric.save() + +@database_sync_to_async +def finish_prompt_metric(prompt_metric, response_length): + print(f'finish_prompt_metric: {response_length}') + prompt_metric.end_time = timezone.now() + prompt_metric.reponse_length = response_length + prompt_metric.event = 'FINISHED' + prompt_metric.save(update_fields=["end_time", "reponse_length","event"]) + print("finish_prompt_metric saved") + +class ChatConsumerAgain(AsyncWebsocketConsumer): + async def connect(self): + await self.accept() + + async def disconnect(self, close_code): + await self.close() + + async def receive(self, text_data=None, bytes_data=None): + print(f"Text Data: {text_data}") + print(f"Bytes Data: {bytes_data}") + if text_data: + data = json.loads(text_data) + message = data.get('message',None) + conversation_id = data.get('conversation_id',None) + email = data.get("email", None) + file = data.get("file", None) + file_type = data.get("fileType", "") + + if not conversation_id: + # we need to create a new conversation + # we will generate a name for it too + conversation_id = await create_conversation(message, email) + + if conversation_id: + decoded_file = None + + if file: + decoded_file = base64.b64decode(file) + print(decoded_file) + if 'csv' in file_type: + file_type = 'csv' + altered_message = f"{message}\n The file type is csv and the file contents are: {decoded_file}" + elif 'xmlformats-officedocument' in file_type: + file_type = 'xlsx' + df = pd.read_excel(decoded_file) + altered_message = f"{message}\n The file type is xlsx and the file contents are: {df}" + elif 'text' in file_type: + file_type = 'txt' + altered_message = f"{message}\n The file type is txt and the file contents are: {decoded_file}" + else: + file_type = 'Not Sure' + + + + + print(f'received: "{message}" for conversation {conversation_id}') + # TODO: add the message to the database + # get the new conversation + # TODO: get the messages here + + messages, prompt = await get_messages(conversation_id, message, decoded_file, file_type) + + prompt_metric = await create_prompt_metric(prompt.id, prompt.message, True if file else False, file_type, MODEL_NAME, conversation_id) + if file: + # udpate with the altered_message + messages = messages[:-1] + [HumanMessage(content=altered_message)] + print(messages) + # send it to the LLM + + # stream the response back + response = "" + # start of the message + await self.send('CONVERSATION_ID') + await self.send(str(conversation_id)) + await self.send('START_OF_THE_STREAM_ENDER_GAME_42') + async for chunk in llm.astream(messages): + print(f"chunk: {chunk}") + response += chunk + await self.send(chunk) + await self.send('END_OF_THE_STREAM_ENDER_GAME_42') + await save_generated_message(conversation_id, response) + + await finish_prompt_metric(prompt_metric, len(response)) + + if bytes_data: + print("we have byte data") diff --git a/llm_be/llm_be/__init__.py b/llm_be/llm_be/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/llm_be/llm_be/asgi.py b/llm_be/llm_be/asgi.py new file mode 100644 index 0000000..f8be107 --- /dev/null +++ b/llm_be/llm_be/asgi.py @@ -0,0 +1,25 @@ +""" +ASGI config for llm_be 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/3.2/howto/deployment/asgi/ +""" + +import os + +from django.core.asgi import get_asgi_application +from channels.routing import ProtocolTypeRouter, URLRouter +from channels.auth import AuthMiddlewareStack +import chat_backend.routing +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'llm_be.settings') + +application = ProtocolTypeRouter({ + "http": get_asgi_application(), + "websocket": AuthMiddlewareStack( + URLRouter( + chat_backend.routing.websocket_urlpatterns + ) + ), + }) diff --git a/llm_be/llm_be/settings.py b/llm_be/llm_be/settings.py new file mode 100644 index 0000000..ab0118b --- /dev/null +++ b/llm_be/llm_be/settings.py @@ -0,0 +1,205 @@ +""" +Django settings for llm_be project. + +Generated by 'django-admin startproject' using Django 3.2.18. + +For more information on this file, see +https://docs.djangoproject.com/en/3.2/topics/settings/ + +For the full list of settings and their values, see +https://docs.djangoproject.com/en/3.2/ref/settings/ +""" + +from pathlib import Path +from datetime import timedelta +import os + +# 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/3.2/howto/deployment/checklist/ + +# SECURITY WARNING: keep the secret key used in production secret! +SECRET_KEY = 'django-insecure-6suk6fj5q2)1tj%)f(wgw1smnliv5-#&@zvgvj1wp#(#@h#31x' + +# SECURITY WARNING: don't run with debug turned on in production! +DEBUG = True + +ALLOWED_HOSTS = ['*.aimloperations.com','localhost','127.0.0.1','chat.aimloperations.com','chatbackend.aimloperations.com'] +CORS_ORIGIN_ALLOW_ALL = True + + +# Application definition + +INSTALLED_APPS = [ + 'daphne', + 'django.contrib.admin', + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.messages', + 'django.contrib.staticfiles', + 'chat_backend', + 'rest_framework', + 'corsheaders', + 'rest_framework_simplejwt.token_blacklist', + +] + +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', + "corsheaders.middleware.CorsMiddleware", + "django.middleware.common.CommonMiddleware", +] + +ROOT_URLCONF = 'llm_be.urls' + +# SETTINGS_PATH = os.path.dirname(os.path.dirname(__file__)) +# TEMPLATE_DIRS = ( +# os.path.join(SETTINGS_PATH, 'templates'), +# ) + +print(os.path.join(BASE_DIR, 'templates')) + +TEMPLATES = [ + { + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'DIRS': [os.path.join(BASE_DIR, 'templates')], + '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 = 'llm_be.wsgi.application' +ASGI_APPLICATION = 'llm_be.asgi.application' + + +# Database +# https://docs.djangoproject.com/en/3.2/ref/settings/#databases + +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.sqlite3', + 'NAME': BASE_DIR / 'db.sqlite3', + } +} + + +# Password validation +# https://docs.djangoproject.com/en/3.2/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/3.2/topics/i18n/ + +LANGUAGE_CODE = 'en-us' + +TIME_ZONE = 'UTC' + +USE_I18N = True + +USE_L10N = True + +USE_TZ = True + + +# Static files (CSS, JavaScript, Images) +# https://docs.djangoproject.com/en/3.2/howto/static-files/ + +STATIC_URL = '/static/' + +# Default primary key field type +# https://docs.djangoproject.com/en/3.2/ref/settings/#default-auto-field + +DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' + +# custom user model +AUTH_USER_MODEL = 'chat_backend.CustomUser' + +# rest framework jwt stuff +REST_FRAMEWORK = { + 'DEFAULT_PERMISSION_CLASSES': ( + 'rest_framework.permissions.IsAuthenticated', + ), + 'DEFAULT_AUTHENTICATION_CLASSES': ( +'rest_framework_simplejwt.authentication.JWTAuthentication', + ), # +} + +SIMPLE_JWT = { + 'ACCESS_TOKEN_LIFETIME':timedelta(hours=5), + 'REFRESH_TOKEN_LIFETIME':timedelta(days=14), + 'ROTATE_REFRESH_TOKENS':True, + 'BLACKLIST_AFTER_ROTATION':True, + 'ALGORITHM':"HS256", + "SIGNING_KEY":SECRET_KEY, + 'VERIFYING_KEY':None, + "AUTH_HEADER_TYPES":('JWT',), + 'USER_ID_FIELD':'id', + 'USER_ID_CLAIM':'user_id', + 'AUTH_TOKEN_CLASSES':('rest_framework_simplejwt.tokens.AccessToken',), + 'TOKEN_TYPE_CLAIM':'token_type', +} + +# CORS settings +CORS_ALLOWED_ORIGINS = [ + "http://localhost:3000", + "http://127.0.0.1:3000", +] + +# channel settings +CHANNEL_LAYERS = { + 'default': { + 'BACKEND': 'channels.layers.InMemoryChannelLayer', + }, +} + +# # Office 365 settings +# EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend' +# EMAIL_HOST = os.getenv("APP_EMAIL_HOST", "smtp.office365.com") +# EMAIL_PORT = os.getenv("APP_EMAIL_PORT", 587) +# EMAIL_HOST_USER = "ryan@aimloperations.com"#os.getenv("APP_EMAIL_HOST_USER") +# SERVER_EMAIL = EMAIL_HOST_USER +# DEFAULT_FROM_EMAIL = EMAIL_HOST_USER +# EMAIL_HOST_PASSWORD = "!HopeThisW0rkz"#os.getenv("APP_EMAIL_HOST_PASSWORD") +# EMAIL_USE_TLS = os.getenv("APP_EMAIL_USE_TLS", True) +# EMAIL_TIMEOUT = os.getenv("APP_EMAIL_TIMEOUT", 60) + +# SMTP2GO +EMAIL_HOST = 'mail.smtp2go.com' +EMAIL_HOST_USER = 'info.aimloperations.com' +EMAIL_HOST_PASSWORD = 'ZDErIII2sipNNVMz' +EMAIL_PORT = 2525 +EMAIL_USE_TLS = True \ No newline at end of file diff --git a/llm_be/llm_be/urls.py b/llm_be/llm_be/urls.py new file mode 100644 index 0000000..16d76aa --- /dev/null +++ b/llm_be/llm_be/urls.py @@ -0,0 +1,24 @@ +"""llm_be URL Configuration + +The `urlpatterns` list routes URLs to views. For more information please see: + https://docs.djangoproject.com/en/3.2/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 +from django.conf import settings +from django.conf.urls.static import static + +urlpatterns = [ + path('admin/', admin.site.urls), + path('api/', include('chat_backend.urls')), +] + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) diff --git a/llm_be/llm_be/wsgi.py b/llm_be/llm_be/wsgi.py new file mode 100644 index 0000000..1edc130 --- /dev/null +++ b/llm_be/llm_be/wsgi.py @@ -0,0 +1,16 @@ +""" +WSGI config for llm_be 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/3.2/howto/deployment/wsgi/ +""" + +import os + +from django.core.wsgi import get_wsgi_application + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'llm_be.settings') + +application = get_wsgi_application() diff --git a/llm_be/manage.py b/llm_be/manage.py new file mode 100755 index 0000000..96e59ea --- /dev/null +++ b/llm_be/manage.py @@ -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', 'llm_be.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() diff --git a/llm_be/scp_command.txt b/llm_be/scp_command.txt new file mode 100644 index 0000000..5ed525b --- /dev/null +++ b/llm_be/scp_command.txt @@ -0,0 +1 @@ +scp -r ./* westfarn@10.0.0.103:/home/westfarn/chat_temp/ diff --git a/requirements.dev b/requirements.dev new file mode 100644 index 0000000..60d210d --- /dev/null +++ b/requirements.dev @@ -0,0 +1,133 @@ +aiohappyeyeballs==2.4.4 +aiohttp==3.11.11 +aiosignal==1.3.2 +annotated-types==0.7.0 +anyio==4.7.0 +asgiref==3.8.1 +astor==0.8.1 +async-timeout==4.0.3 +attrs==24.3.0 +autobahn==24.4.2 +Automat==24.8.1 +black==24.10.0 +certifi==2024.12.14 +cffi==1.17.1 +channels==4.2.0 +charset-normalizer==3.4.1 +click==8.1.8 +constantly==23.10.4 +contourpy==1.3.0 +cryptography==44.0.0 +cycler==0.12.1 +daphne==4.1.2 +dataclasses-json==0.6.7 +distro==1.9.0 +Django==4.2.17 +django-autoslug==1.9.9 +django-cors-headers==4.6.0 +djangorestframework==3.15.2 +djangorestframework-simplejwt==5.3.1 +duckdb==1.1.3 +et_xmlfile==2.0.0 +exceptiongroup==1.2.2 +Faker==33.1.0 +filelock==3.16.1 +fonttools==4.55.3 +frozenlist==1.5.0 +fsspec==2024.12.0 +greenlet==3.1.1 +h11==0.14.0 +httpcore==1.0.7 +httpx==0.27.2 +httpx-sse==0.4.0 +hyperlink==21.0.0 +idna==3.10 +importlib_resources==6.4.5 +incremental==24.7.2 +Jinja2==3.1.5 +jiter==0.8.2 +jsonpatch==1.33 +jsonpointer==3.0.0 +kiwisolver==1.4.7 +langchain==0.3.13 +langchain-community==0.3.13 +langchain-core==0.3.28 +langchain-ollama==0.2.2 +langchain-openai==0.2.14 +langchain-text-splitters==0.3.4 +langsmith==0.2.7 +lxml==5.3.0 +MarkupSafe==3.0.2 +marshmallow==3.23.2 +matplotlib==3.9.4 +mpmath==1.3.0 +multidict==6.1.0 +mypy-extensions==1.0.0 +networkx==3.2.1 +numpy==2.0.2 +nvidia-cublas-cu12==12.4.5.8 +nvidia-cuda-cupti-cu12==12.4.127 +nvidia-cuda-nvrtc-cu12==12.4.127 +nvidia-cuda-runtime-cu12==12.4.127 +nvidia-cudnn-cu12==9.1.0.70 +nvidia-cufft-cu12==11.2.1.3 +nvidia-curand-cu12==10.3.5.147 +nvidia-cusolver-cu12==11.6.1.9 +nvidia-cusparse-cu12==12.3.1.170 +nvidia-nccl-cu12==2.21.5 +nvidia-nvjitlink-cu12==12.4.127 +nvidia-nvtx-cu12==12.4.127 +ollama==0.4.5 +ollama-python==0.1.2 +openai==1.58.1 +openpyxl==3.1.5 +orjson==3.10.13 +packaging==24.2 +pandas==2.2.3 +pandasai==2.4.1 +pathspec==0.12.1 +pillow==11.0.0 +platformdirs==4.3.6 +propcache==0.2.1 +pyasn1==0.6.1 +pyasn1_modules==0.4.1 +pycparser==2.22 +pydantic==2.10.4 +pydantic-settings==2.7.1 +pydantic_core==2.27.2 +PyJWT==2.10.1 +pyOpenSSL==24.3.0 +pyparsing==3.2.0 +python-dateutil==2.9.0.post0 +python-docx==1.1.2 +python-dotenv==1.0.1 +pytz==2024.2 +PyYAML==6.0.2 +regex==2024.11.6 +requests==2.32.3 +requests-toolbelt==1.0.0 +responses==0.25.3 +scipy==1.13.1 +service-identity==24.2.0 +six==1.17.0 +sniffio==1.3.1 +SQLAlchemy==2.0.36 +sqlglot==26.0.1 +sqlglotrs==0.3.0 +sqlparse==0.5.3 +sympy==1.13.1 +tenacity==9.0.0 +tiktoken==0.8.0 +tomli==2.2.1 +torch==2.5.1 +tqdm==4.67.1 +triton==3.1.0 +Twisted==24.11.0 +txaio==23.1.1 +typing-inspect==0.9.0 +typing_extensions==4.12.2 +tzdata==2024.2 +urllib3==2.3.0 +yarl==1.18.3 +zipp==3.21.0 +zope.interface==7.2 diff --git a/upgrade_all_package.sh b/upgrade_all_package.sh new file mode 100755 index 0000000..c428a0b --- /dev/null +++ b/upgrade_all_package.sh @@ -0,0 +1 @@ +python -m pip --disable-pip-version-check list --outdated --format=json | python -c "import json, sys; print('\n'.join([x['name'] for x in json.load(sys.stdin)]))" | xargs -n1 pip install -U