From 2b3c52d2fe2473ad28436e43cc4a315677505648 Mon Sep 17 00:00:00 2001 From: RealCifer <123382295+RealCifer@users.noreply.github.com> Date: Fri, 30 Jan 2026 03:08:52 +0530 Subject: [PATCH 1/6] auto-generate conversation summary Email - adityakhamait12@gmail.com --- backend/chat/models.py | 36 +++++++++++++++++++++++++++++++++--- 1 file changed, 33 insertions(+), 3 deletions(-) diff --git a/backend/chat/models.py b/backend/chat/models.py index 242788f14..fc22b5a82 100644 --- a/backend/chat/models.py +++ b/backend/chat/models.py @@ -1,6 +1,7 @@ import uuid from django.db import models +from django.apps import apps from authentication.models import CustomUser @@ -15,6 +16,13 @@ def __str__(self): class Conversation(models.Model): id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) title = models.CharField(max_length=100, blank=False, null=False, default="Mock title") + + summary = models.TextField( + blank=True, + null=True, + help_text="Auto-generated summary of the conversation" + ) + created_at = models.DateTimeField(auto_now_add=True) modified_at = models.DateTimeField(auto_now=True) active_version = models.ForeignKey( @@ -31,6 +39,29 @@ def version_count(self): version_count.short_description = "Number of versions" + def generate_summary(self): + """ + Generate a simple summary using the first few messages + """ + Message = apps.get_model("chat", "Message") + + messages = ( + Message.objects + .filter(version__conversation=self) + .order_by("created_at") + .values_list("content", flat=True)[:3] + ) + + if messages: + return " | ".join(messages) + + return "" + + def save(self, *args, **kwargs): + if not self.summary: + self.summary = self.generate_summary() + super().save(*args, **kwargs) + class Version(models.Model): id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) @@ -43,8 +74,7 @@ class Version(models.Model): def __str__(self): if self.root_message: return f"Version of `{self.conversation.title}` created at `{self.root_message.created_at}`" - else: - return f"Version of `{self.conversation.title}` with no root message yet" + return f"Version of `{self.conversation.title}` with no root message yet" class Message(models.Model): @@ -58,8 +88,8 @@ class Meta: ordering = ["created_at"] def save(self, *args, **kwargs): - self.version.conversation.save() super().save(*args, **kwargs) + self.version.conversation.save() def __str__(self): return f"{self.role}: {self.content[:20]}..." From f24508cd29b2dd8f38603120053b45eeb0670cc9 Mon Sep 17 00:00:00 2001 From: RealCifer <123382295+RealCifer@users.noreply.github.com> Date: Fri, 30 Jan 2026 03:39:36 +0530 Subject: [PATCH 2/6] expose conversation summary in API --- backend/chat/serializers.py | 93 +++++++++++++++++++------------------ 1 file changed, 47 insertions(+), 46 deletions(-) diff --git a/backend/chat/serializers.py b/backend/chat/serializers.py index 0c721c061..43acdd09c 100644 --- a/backend/chat/serializers.py +++ b/backend/chat/serializers.py @@ -6,9 +6,7 @@ def should_serialize(validated_data, field_name) -> bool: - if validated_data.get(field_name) is not None: - return True - + return validated_data.get(field_name) is not None class TitleSerializer(serializers.Serializer): title = serializers.CharField(max_length=100, required=True) @@ -18,62 +16,61 @@ class VersionTimeIdSerializer(serializers.Serializer): id = serializers.UUIDField() created_at = serializers.DateTimeField() - class MessageSerializer(serializers.ModelSerializer): - role = serializers.SlugRelatedField(slug_field="name", queryset=Role.objects.all()) + role = serializers.SlugRelatedField( + slug_field="name", + queryset=Role.objects.all() + ) class Meta: model = Message fields = [ - "id", # DB + "id", "content", - "role", # required - "created_at", # DB, read-only + "role", + "created_at", ] read_only_fields = ["id", "created_at", "version"] def create(self, validated_data): - message = Message.objects.create(**validated_data) - return message + return Message.objects.create(**validated_data) def to_representation(self, instance): representation = super().to_representation(instance) - representation["versions"] = [] # add versions field + representation["versions"] = [] return representation - class VersionSerializer(serializers.ModelSerializer): messages = MessageSerializer(many=True) active = serializers.SerializerMethodField() - conversation_id = serializers.UUIDField(source="conversation.id") + conversation_id = serializers.UUIDField(source="conversation.id", read_only=True) created_at = serializers.SerializerMethodField() class Meta: model = Version fields = [ "id", - "conversation_id", # DB + "conversation_id", "root_message", "messages", "active", - "created_at", # DB, read-only - "parent_version", # optional + "created_at", + "parent_version", ] read_only_fields = ["id", "conversation"] - @staticmethod - def get_active(obj): + def get_active(self, obj): return obj == obj.conversation.active_version - @staticmethod - def get_created_at(obj): - if obj.root_message is None: - return timezone.localtime(obj.conversation.created_at) - return timezone.localtime(obj.root_message.created_at) + def get_created_at(self, obj): + if obj.root_message: + return timezone.localtime(obj.root_message.created_at) + return timezone.localtime(obj.conversation.created_at) def create(self, validated_data): - messages_data = validated_data.pop("messages") + messages_data = validated_data.pop("messages", []) version = Version.objects.create(**validated_data) + for message_data in messages_data: Message.objects.create(version=version, **message_data) @@ -83,16 +80,15 @@ def update(self, instance, validated_data): instance.conversation = validated_data.get("conversation", instance.conversation) instance.parent_version = validated_data.get("parent_version", instance.parent_version) instance.root_message = validated_data.get("root_message", instance.root_message) + if not any( - [ - should_serialize(validated_data, "conversation"), - should_serialize(validated_data, "parent_version"), - should_serialize(validated_data, "root_message"), - ] + should_serialize(validated_data, field) + for field in ["conversation", "parent_version", "root_message"] ): raise ValidationError( - "At least one of the following fields must be provided: conversation, parent_version, root_message" + "At least one of conversation, parent_version, or root_message must be provided." ) + instance.save() messages_data = validated_data.pop("messages", []) @@ -107,46 +103,51 @@ def update(self, instance, validated_data): return instance - class ConversationSerializer(serializers.ModelSerializer): versions = VersionSerializer(many=True) + summary = serializers.CharField(read_only=True) class Meta: model = Conversation fields = [ - "id", # DB - "title", # required + "id", + "title", + "summary", "active_version", - "versions", # optional - "modified_at", # DB, read-only + "versions", + "modified_at", ] + read_only_fields = ["id", "summary", "modified_at"] def create(self, validated_data): versions_data = validated_data.pop("versions", []) conversation = Conversation.objects.create(**validated_data) + for version_data in versions_data: - version_serializer = VersionSerializer(data=version_data) - if version_serializer.is_valid(): - version_serializer.save(conversation=conversation) + serializer = VersionSerializer(data=version_data) + serializer.is_valid(raise_exception=True) + serializer.save(conversation=conversation) return conversation def update(self, instance, validated_data): instance.title = validated_data.get("title", instance.title) - active_version_id = validated_data.get("active_version", instance.active_version) - if active_version_id is not None: - active_version = Version.objects.get(id=active_version_id) - instance.active_version = active_version + + active_version_id = validated_data.get("active_version") + if active_version_id: + instance.active_version = Version.objects.get(id=active_version_id) + instance.save() versions_data = validated_data.pop("versions", []) for version_data in versions_data: if "id" in version_data: version = Version.objects.get(id=version_data["id"], conversation=instance) - version_serializer = VersionSerializer(version, data=version_data) + serializer = VersionSerializer(version, data=version_data) else: - version_serializer = VersionSerializer(data=version_data) - if version_serializer.is_valid(): - version_serializer.save(conversation=instance) + serializer = VersionSerializer(data=version_data) + + serializer.is_valid(raise_exception=True) + serializer.save(conversation=instance) return instance From c04025fdc08323451c652accf742062582b4b601 Mon Sep 17 00:00:00 2001 From: RealCifer <123382295+RealCifer@users.noreply.github.com> Date: Fri, 30 Jan 2026 03:43:04 +0530 Subject: [PATCH 3/6] chore: update gitignore to exclude local files --- .gitignore | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/.gitignore b/.gitignore index 2e2ba01e5..08ba48bc9 100644 --- a/.gitignore +++ b/.gitignore @@ -31,3 +31,15 @@ server/dist # prompts prompts/debug +# Virtual environment +venv/ + +# Environment variables +.env + +# Python cache +__pycache__/ +*.pyc + +# DB +db.sqlite3 \ No newline at end of file From 6534dc1e50fa4cd95b7b3750ee3212a1d736a28f Mon Sep 17 00:00:00 2001 From: RealCifer <123382295+RealCifer@users.noreply.github.com> Date: Sat, 31 Jan 2026 01:15:09 +0530 Subject: [PATCH 4/6] migrate database configuration to PostgreSQL --- backend/backend/settings.py | 29 ++++++----------------------- 1 file changed, 6 insertions(+), 23 deletions(-) diff --git a/backend/backend/settings.py b/backend/backend/settings.py index 9de4f024a..6ef6f5477 100644 --- a/backend/backend/settings.py +++ b/backend/backend/settings.py @@ -17,23 +17,15 @@ load_dotenv() -# 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/4.2/howto/deployment/checklist/ - -# SECURITY WARNING: keep the secret key used in production secret! SECRET_KEY = os.environ["DJANGO_SECRET_KEY"] FRONTEND_URL = os.environ["FRONTEND_URL"] -# SECURITY WARNING: don't run with debug turned on in production! DEBUG = True ALLOWED_HOSTS = [] -# Application definition - INSTALLED_APPS = [ "django.contrib.admin", "django.contrib.auth", @@ -81,18 +73,17 @@ WSGI_APPLICATION = "backend.wsgi.application" -# Database -# https://docs.djangoproject.com/en/4.2/ref/settings/#databases - DATABASES = { "default": { - "ENGINE": "django.db.backends.sqlite3", - "NAME": BASE_DIR / "db.sqlite3", + "ENGINE": "django.db.backends.postgresql", + "NAME": os.getenv("DB_NAME", "fullstack_db"), + "USER": os.getenv("DB_USER", "fullstack_user"), + "PASSWORD": os.getenv("DB_PASSWORD"), + "HOST": "localhost", + "PORT": "5432", } } -# Password validation -# https://docs.djangoproject.com/en/4.2/ref/settings/#auth-password-validators AUTH_PASSWORD_VALIDATORS = [ { @@ -109,16 +100,12 @@ }, ] -# Custom user model AUTH_USER_MODEL = "authentication.CustomUser" AUTHENTICATION_BACKENDS = [ "django.contrib.auth.backends.ModelBackend", ] -# Internationalization -# https://docs.djangoproject.com/en/4.2/topics/i18n/ - LANGUAGE_CODE = "en-us" TIME_ZONE = "Europe/Warsaw" @@ -126,14 +113,10 @@ USE_I18N = True -# Static files (CSS, JavaScript, Images) -# https://docs.djangoproject.com/en/4.2/howto/static-files/ STATIC_ROOT = BASE_DIR / "static" STATIC_URL = "/static/" -# Default primary key field type -# https://docs.djangoproject.com/en/4.2/ref/settings/#default-auto-field DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" From c95d975e8ae730669361d13045c190febe595bb6 Mon Sep 17 00:00:00 2001 From: RealCifer <123382295+RealCifer@users.noreply.github.com> Date: Sat, 31 Jan 2026 01:43:02 +0530 Subject: [PATCH 5/6] fix: add missing conversation summary migration --- .../migrations/0002_conversation_summary.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 backend/chat/migrations/0002_conversation_summary.py diff --git a/backend/chat/migrations/0002_conversation_summary.py b/backend/chat/migrations/0002_conversation_summary.py new file mode 100644 index 000000000..4d5053475 --- /dev/null +++ b/backend/chat/migrations/0002_conversation_summary.py @@ -0,0 +1,18 @@ +# Generated by Django 6.0.1 on 2026-01-30 20:11 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('chat', '0001_initial'), + ] + + operations = [ + migrations.AddField( + model_name='conversation', + name='summary', + field=models.TextField(blank=True, help_text='Auto-generated summary of the conversation', null=True), + ), + ] From 47329deb06050821e0586dca934061b711266529 Mon Sep 17 00:00:00 2001 From: RealCifer <123382295+RealCifer@users.noreply.github.com> Date: Sat, 31 Jan 2026 01:45:20 +0530 Subject: [PATCH 6/6] task 2.2: migrate to PostgreSQL and add cleanup management command --- backend/backend/settings.py | 8 ++-- .../commands/cleanup_conversations.py | 39 +++++++++++++++++++ 2 files changed, 43 insertions(+), 4 deletions(-) create mode 100644 backend/chat/management/commands/cleanup_conversations.py diff --git a/backend/backend/settings.py b/backend/backend/settings.py index 6ef6f5477..5c6433e2d 100644 --- a/backend/backend/settings.py +++ b/backend/backend/settings.py @@ -76,10 +76,10 @@ DATABASES = { "default": { "ENGINE": "django.db.backends.postgresql", - "NAME": os.getenv("DB_NAME", "fullstack_db"), - "USER": os.getenv("DB_USER", "fullstack_user"), - "PASSWORD": os.getenv("DB_PASSWORD"), - "HOST": "localhost", + "NAME": "fullstack_db", + "USER": "fullstack_user", + "PASSWORD": "fullstack123", + "HOST": "127.0.0.1", "PORT": "5432", } } diff --git a/backend/chat/management/commands/cleanup_conversations.py b/backend/chat/management/commands/cleanup_conversations.py new file mode 100644 index 000000000..5d3c96543 --- /dev/null +++ b/backend/chat/management/commands/cleanup_conversations.py @@ -0,0 +1,39 @@ +from datetime import timedelta + +from django.core.management.base import BaseCommand +from django.utils import timezone + +from chat.models import Conversation + + +class Command(BaseCommand): + help = "Soft delete conversations older than given number of days" + + def add_arguments(self, parser): + parser.add_argument( + "--days", + type=int, + default=30, + help="Delete conversations older than this many days", + ) + + def handle(self, *args, **options): + days = options["days"] + cutoff_date = timezone.now() - timedelta(days=days) + + conversations = Conversation.objects.filter( + modified_at__lt=cutoff_date, + deleted_at__isnull=True, + ) + + count = conversations.count() + + for convo in conversations: + convo.deleted_at = timezone.now() + convo.save(update_fields=["deleted_at"]) + + self.stdout.write( + self.style.SUCCESS( + f"Soft-deleted {count} conversation(s) older than {days} days" + ) + )