diff --git a/backend/chat/admin.py b/backend/chat/admin.py index a4e7d15fc..7d7ee5b1f 100644 --- a/backend/chat/admin.py +++ b/backend/chat/admin.py @@ -20,7 +20,7 @@ def display_desc(self, obj): class MessageInline(NestedTabularInline): model = Message - extra = 2 # number of extra forms to display + extra = 2 class VersionInline(NestedStackedInline): @@ -51,7 +51,17 @@ def queryset(self, request, queryset): class ConversationAdmin(NestedModelAdmin): actions = ["undelete_selected", "soft_delete_selected"] inlines = [VersionInline] - list_display = ("title", "id", "created_at", "modified_at", "deleted_at", "version_count", "is_deleted", "user") + list_display = ( + "title", + "id", + "created_at", + "modified_at", + "deleted_at", + "version_count", + "is_deleted", + "user", + "summary", # 🔥 Show summary in admin + ) list_filter = (DeletedListFilter,) ordering = ("-modified_at",) @@ -68,10 +78,8 @@ def soft_delete_selected(self, request, queryset): def get_action_choices(self, request, **kwargs): choices = super().get_action_choices(request) for idx, choice in enumerate(choices): - fn_name = choice[0] - if fn_name == "delete_selected": - new_choice = (fn_name, "Hard delete selected conversations") - choices[idx] = new_choice + if choice[0] == "delete_selected": + choices[idx] = (choice[0], "Hard delete selected conversations") return choices def is_deleted(self, obj): diff --git a/backend/chat/migrations/0002_conversation_summary.py b/backend/chat/migrations/0002_conversation_summary.py new file mode 100644 index 000000000..26a0db7ea --- /dev/null +++ b/backend/chat/migrations/0002_conversation_summary.py @@ -0,0 +1,18 @@ +# Generated by Django 5.2.4 on 2025-07-28 06:26 + +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, null=True), + ), + ] diff --git a/backend/chat/migrations/0003_remove_conversation_active_version_and_more.py b/backend/chat/migrations/0003_remove_conversation_active_version_and_more.py new file mode 100644 index 000000000..7d6a6388d --- /dev/null +++ b/backend/chat/migrations/0003_remove_conversation_active_version_and_more.py @@ -0,0 +1,30 @@ +# Generated by Django 5.2.4 on 2025-07-28 07:21 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('chat', '0002_conversation_summary'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.RemoveField( + model_name='conversation', + name='active_version', + ), + migrations.AlterField( + model_name='conversation', + name='title', + field=models.CharField(blank=True, max_length=255, null=True), + ), + migrations.AlterField( + model_name='conversation', + name='user', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='conversations', to=settings.AUTH_USER_MODEL), + ), + ] diff --git a/backend/chat/migrations/0004_conversation_active_version_alter_conversation_title_and_more.py b/backend/chat/migrations/0004_conversation_active_version_alter_conversation_title_and_more.py new file mode 100644 index 000000000..ed4b47132 --- /dev/null +++ b/backend/chat/migrations/0004_conversation_active_version_alter_conversation_title_and_more.py @@ -0,0 +1,31 @@ +# Generated by Django 5.2.4 on 2025-07-28 09:03 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('chat', '0003_remove_conversation_active_version_and_more'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.AddField( + model_name='conversation', + name='active_version', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='current_version_conversations', to='chat.version'), + ), + migrations.AlterField( + model_name='conversation', + name='title', + field=models.CharField(default='Mock title', max_length=100), + ), + migrations.AlterField( + model_name='conversation', + name='user', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL), + ), + ] diff --git a/backend/chat/models.py b/backend/chat/models.py index 242788f14..387970598 100644 --- a/backend/chat/models.py +++ b/backend/chat/models.py @@ -1,12 +1,11 @@ import uuid - from django.db import models - from authentication.models import CustomUser +from chat.utils.summarizer import generate_summary # Your own T5-based summarizer class Role(models.Model): - name = models.CharField(max_length=20, blank=False, null=False, default="user") + name = models.CharField(max_length=20, default="user") def __str__(self): return self.name @@ -14,7 +13,7 @@ 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") + title = models.CharField(max_length=100, default="Mock title") created_at = models.DateTimeField(auto_now_add=True) modified_at = models.DateTimeField(auto_now=True) active_version = models.ForeignKey( @@ -22,6 +21,7 @@ class Conversation(models.Model): ) deleted_at = models.DateTimeField(null=True, blank=True) user = models.ForeignKey(CustomUser, on_delete=models.CASCADE) + summary = models.TextField(blank=True, null=True) # ✅ Summary field def __str__(self): return self.title @@ -31,6 +31,14 @@ def version_count(self): version_count.short_description = "Number of versions" + def update_summary(self): + from .models import Message # Avoid circular import + messages = Message.objects.filter(version__conversation=self).order_by("created_at") + combined_text = " ".join(msg.content for msg in messages) + if combined_text.strip(): + self.summary = generate_summary(combined_text) + self.save() + class Version(models.Model): id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) @@ -41,15 +49,12 @@ 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}`" class Message(models.Model): id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) - content = models.TextField(blank=False, null=False) + content = models.TextField() role = models.ForeignKey(Role, on_delete=models.CASCADE) created_at = models.DateTimeField(auto_now_add=True) version = models.ForeignKey("Version", related_name="messages", on_delete=models.CASCADE) @@ -58,8 +63,8 @@ class Meta: ordering = ["created_at"] def save(self, *args, **kwargs): - self.version.conversation.save() super().save(*args, **kwargs) + self.version.conversation.update_summary() # ✅ Auto-trigger on message save def __str__(self): return f"{self.role}: {self.content[:20]}..." diff --git a/backend/chat/utils/summa b/backend/chat/utils/summa new file mode 100644 index 000000000..e69de29bb diff --git a/backend/chat/utils/summarizer.py b/backend/chat/utils/summarizer.py new file mode 100644 index 000000000..2b5cf0c6f --- /dev/null +++ b/backend/chat/utils/summarizer.py @@ -0,0 +1,15 @@ +from transformers import pipeline + +# Load once and reuse +summarizer = pipeline("summarization", model="t5-small", tokenizer="t5-small") + +def generate_summary(text: str) -> str: + if not text.strip(): + return "No content available for summary." + try: + # Truncate if too long for small model + text = text[:1000] + summary = summarizer(text, max_length=60, min_length=20, do_sample=False) + return summary[0]['summary_text'] + except Exception as e: + return f"Summary error: {str(e)}"