diff --git a/backend/backend/__init__.py b/backend/backend/__init__.py index e69de29bb..63c59b210 100644 --- a/backend/backend/__init__.py +++ b/backend/backend/__init__.py @@ -0,0 +1,3 @@ +from .celery import app as celery_app + +_all_ = ('celery_app',) \ No newline at end of file diff --git a/backend/backend/celery.py b/backend/backend/celery.py new file mode 100644 index 000000000..badc78c56 --- /dev/null +++ b/backend/backend/celery.py @@ -0,0 +1,12 @@ +# backend/celery.py + +import os +from celery import Celery + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'backend.settings') + +app = Celery('backend') + +app.config_from_object('django.conf:settings', namespace='CELERY') + +app.autodiscover_tasks() \ No newline at end of file diff --git a/backend/backend/settings.py b/backend/backend/settings.py index 9de4f024a..6ac83e29c 100644 --- a/backend/backend/settings.py +++ b/backend/backend/settings.py @@ -24,8 +24,15 @@ # 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"] +# SECRET_KEY = os.environ["DJANGO_SECRET_KEY"] +# FRONTEND_URL = os.environ["FRONTEND_URL"] + + +from decouple import config +SECRET_KEY=config("DJANGO_SECRET_KEY") + +# FRONTEND_URL +FRONTEND_URL=config("FRONTEND_URL",default="http://localhost:3000") # SECURITY WARNING: don't run with debug turned on in production! DEBUG = True @@ -47,6 +54,7 @@ "authentication", "chat", "gpt", + 'django_filters', ] MIDDLEWARE = [ @@ -84,10 +92,15 @@ # Database # https://docs.djangoproject.com/en/4.2/ref/settings/#databases + DATABASES = { - "default": { - "ENGINE": "django.db.backends.sqlite3", - "NAME": BASE_DIR / "db.sqlite3", + 'default': { + 'ENGINE': 'django.db.backends.postgresql', + 'NAME': 'fs_ai_db', + 'USER': 'postgres', + 'PASSWORD': '12345', + 'HOST': 'localhost', + 'PORT': '5432' } } @@ -149,3 +162,31 @@ SESSION_COOKIE_SECURE = True CSRF_COOKIE_SECURE = True CSRF_COOKIE_SAMESITE = "None" + + +# CELERY SETTINGS +CELERY_BROKER_URL = 'redis://localhost:6379/0' +CELERY_ACCEPT_CONTENT = ['json'] +CELERY_TASK_SERIALIZER = 'json' + +# CELERY BEAT SETTINGS +INSTALLED_APPS += ['django_celery_beat'] + +REST_FRAMEWORK = { + 'DEFAULT_FILTER_BACKENDS': ['django_filters.rest_framework.DjangoFilterBackend'], + 'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination', + 'PAGE_SIZE': 5, +} + + +LOGGING = { + 'version': 1, + 'disable_existing_loggers': False, + 'handlers': { + 'console': { 'class': 'logging.StreamHandler' }, + }, + 'root': { + 'handlers': ['console'], + 'level': 'INFO', + }, +} \ No newline at end of file diff --git a/backend/chat/admin.py b/backend/chat/admin.py index a4e7d15fc..c069b3ad1 100644 --- a/backend/chat/admin.py +++ b/backend/chat/admin.py @@ -1,34 +1,69 @@ from django.contrib import admin from django.utils import timezone +from django.core.exceptions import ValidationError +from django.forms import ModelForm from nested_admin.nested import NestedModelAdmin, NestedStackedInline, NestedTabularInline -from chat.models import Conversation, Message, Role, Version +from chat.models import Conversation, Message, Role, Version, FileUpload + + +# Custom Form for FileUpload + +class FileUploadForm(ModelForm): + class Meta: + model = FileUpload + fields = '__all__' + + def clean(self): + cleaned_data = super().clean() + file = cleaned_data.get('file') + + if file: + import hashlib + hash_obj = hashlib.sha256() + for chunk in file.chunks(): + hash_obj.update(chunk) + file_hash = hash_obj.hexdigest() + + # Check if the same file already exists + if FileUpload.objects.filter(file_hash=file_hash).exclude(pk=self.instance.pk).exists(): + raise ValidationError("⚠️ File already exists in the system (duplicate content).") + + # Store the hash + cleaned_data['file_hash'] = file_hash + + return cleaned_data + + +class FileUploadAdmin(admin.ModelAdmin): + form = FileUploadForm + readonly_fields = ('file_hash', 'uploaded_at') # Hide from editable fields + fields = ('file', 'original_name') # Only show editable fields + list_display = ('original_name', 'file_hash', 'uploaded_at') # Optional display + + +# Rest of your admin setup class RoleAdmin(NestedModelAdmin): list_display = ["id", "name"] - class MessageAdmin(NestedModelAdmin): list_display = ["display_desc", "role", "id", "created_at", "version"] def display_desc(self, obj): return obj.content[:20] + "..." - display_desc.short_description = "content" - class MessageInline(NestedTabularInline): model = Message - extra = 2 # number of extra forms to display - + extra = 2 class VersionInline(NestedStackedInline): model = Version extra = 1 inlines = [MessageInline] - class DeletedListFilter(admin.SimpleListFilter): title = "Deleted" parameter_name = "deleted" @@ -40,53 +75,50 @@ def lookups(self, request, model_admin): ) def queryset(self, request, queryset): - value = self.value() - if value == "True": + if self.value() == "True": return queryset.filter(deleted_at__isnull=False) - elif value == "False": + elif self.value() == "False": return queryset.filter(deleted_at__isnull=True) return 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_filter = (DeletedListFilter,) + list_display = ("title", "id", "created_at", "modified_at", "deleted_at", "version_count", "is_deleted", "user", "summary") + list_filter = (DeletedListFilter, 'created_at', 'user') + search_fields = ('title', 'summary', 'user__username') + list_per_page = 10 ordering = ("-modified_at",) def undelete_selected(self, request, queryset): queryset.update(deleted_at=None) - undelete_selected.short_description = "Undelete selected conversations" def soft_delete_selected(self, request, queryset): queryset.update(deleted_at=timezone.now()) - soft_delete_selected.short_description = "Soft delete selected conversations" 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] = ("delete_selected", "Hard delete selected conversations") return choices def is_deleted(self, obj): return obj.deleted_at is not None - is_deleted.boolean = True is_deleted.short_description = "Deleted?" - class VersionAdmin(NestedModelAdmin): inlines = [MessageInline] list_display = ("id", "conversation", "parent_version", "root_message") +# Register all models + admin.site.register(Role, RoleAdmin) admin.site.register(Message, MessageAdmin) admin.site.register(Conversation, ConversationAdmin) admin.site.register(Version, VersionAdmin) +admin.site.register(FileUpload, FileUploadAdmin) diff --git a/backend/chat/apps.py b/backend/chat/apps.py index 5f75238d2..ff0660c27 100644 --- a/backend/chat/apps.py +++ b/backend/chat/apps.py @@ -2,5 +2,8 @@ class ChatConfig(AppConfig): - default_auto_field = "django.db.models.BigAutoField" - name = "chat" + default_auto_field = 'django.db.models.BigAutoField' + name = 'chat' + + def ready(self): + import chat.signals \ No newline at end of file diff --git a/backend/chat/management/commands/cleanup_old_conversations.py b/backend/chat/management/commands/cleanup_old_conversations.py new file mode 100644 index 000000000..c9d7d0b6a --- /dev/null +++ b/backend/chat/management/commands/cleanup_old_conversations.py @@ -0,0 +1,20 @@ +from django.core.management.base import BaseCommand +from django.utils import timezone +from chat.models import Conversation +from datetime import timedelta + +class Command(BaseCommand): + help = 'Soft-deletes conversations older than 30 days' + + def handle(self, *args, **kwargs): + days = 30 + cutoff_date = timezone.now() - timedelta(days=days) + + deleted_count = Conversation.objects.filter( + created_at__lt=cutoff_date, + deleted_at__isnull=True + ).update(deleted_at=timezone.now()) + + self.stdout.write( + self.style.SUCCESS(f"{deleted_count} conversations soft-deleted.") + ) \ No newline at end of file diff --git a/backend/chat/migrations/0002_conversation_summary.py b/backend/chat/migrations/0002_conversation_summary.py new file mode 100644 index 000000000..34e33ad07 --- /dev/null +++ b/backend/chat/migrations/0002_conversation_summary.py @@ -0,0 +1,17 @@ +# Generated by Django 5.0.2 on 2025-07-05 09:04 + +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_fileupload.py b/backend/chat/migrations/0003_fileupload.py new file mode 100644 index 000000000..a36e60755 --- /dev/null +++ b/backend/chat/migrations/0003_fileupload.py @@ -0,0 +1,22 @@ +# Generated by Django 5.0.2 on 2025-07-07 10:47 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("chat", "0002_conversation_summary"), + ] + + operations = [ + migrations.CreateModel( + name="FileUpload", + fields=[ + ("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), + ("file", models.FileField(upload_to="uploads/")), + ("file_hash", models.CharField(max_length=64, unique=True)), + ("original_name", models.CharField(max_length=255)), + ("uploaded_at", models.DateTimeField(auto_now_add=True)), + ], + ), + ] diff --git a/backend/chat/migrations/0004_filelog.py b/backend/chat/migrations/0004_filelog.py new file mode 100644 index 000000000..4b4542615 --- /dev/null +++ b/backend/chat/migrations/0004_filelog.py @@ -0,0 +1,35 @@ +# Generated by Django 5.0.2 on 2025-07-08 12:45 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("chat", "0003_fileupload"), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name="FileLog", + fields=[ + ("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), + ("file_name", models.CharField(max_length=255)), + ( + "action", + models.CharField( + choices=[("UPLOAD", "Upload"), ("DELETE", "Delete"), ("ACCESS", "Access")], max_length=10 + ), + ), + ("timestamp", models.DateTimeField(auto_now_add=True)), + ( + "user", + models.ForeignKey( + null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL + ), + ), + ], + ), + ] diff --git a/backend/chat/migrations/0005_alter_fileupload_file_hash.py b/backend/chat/migrations/0005_alter_fileupload_file_hash.py new file mode 100644 index 000000000..eb815acb1 --- /dev/null +++ b/backend/chat/migrations/0005_alter_fileupload_file_hash.py @@ -0,0 +1,17 @@ +# Generated by Django 5.0.2 on 2025-07-08 13:07 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("chat", "0004_filelog"), + ] + + operations = [ + migrations.AlterField( + model_name="fileupload", + name="file_hash", + field=models.CharField(max_length=64), + ), + ] diff --git a/backend/chat/migrations/0006_alter_fileupload_file_hash.py b/backend/chat/migrations/0006_alter_fileupload_file_hash.py new file mode 100644 index 000000000..75050c571 --- /dev/null +++ b/backend/chat/migrations/0006_alter_fileupload_file_hash.py @@ -0,0 +1,17 @@ +# Generated by Django 5.0.2 on 2025-07-08 13:10 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("chat", "0005_alter_fileupload_file_hash"), + ] + + operations = [ + migrations.AlterField( + model_name="fileupload", + name="file_hash", + field=models.CharField(max_length=64, unique=True), + ), + ] diff --git a/backend/chat/models.py b/backend/chat/models.py index 242788f14..80434c5d7 100644 --- a/backend/chat/models.py +++ b/backend/chat/models.py @@ -1,8 +1,8 @@ import uuid - from django.db import models - from authentication.models import CustomUser +import hashlib +from django.db import models class Role(models.Model): @@ -22,6 +22,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) def __str__(self): return self.title @@ -53,6 +54,7 @@ class Message(models.Model): 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) + class Meta: ordering = ["created_at"] @@ -63,3 +65,42 @@ def save(self, *args, **kwargs): def __str__(self): return f"{self.role}: {self.content[:20]}..." + +from django.core.exceptions import ValidationError +class FileUpload(models.Model): + file = models.FileField(upload_to='uploads/') + file_hash = models.CharField(max_length=64, unique=True) + original_name = models.CharField(max_length=255) + uploaded_at = models.DateTimeField(auto_now_add=True) + + def clean(self): + if FileUpload.objects.exclude(pk=self.pk).filter(file_hash=self.file_hash).exists(): + raise ValidationError("This file already exists in the system.") + + def save(self, *args, **kwargs): + if not self.file_hash: + sha256 = hashlib.sha256() + for chunk in self.file.chunks(): + sha256.update(chunk) + self.file_hash = sha256.hexdigest() + super().save(*args, **kwargs) + + def __str__(self): + return self.original_name + + +# task-4 + +class FileLog(models.Model): + ACTION_CHOICES = [ + ('UPLOAD', 'Upload'), + ('DELETE', 'Delete'), + ('ACCESS', 'Access'), + ] + user = models.ForeignKey(CustomUser, on_delete=models.SET_NULL, null=True) + file_name = models.CharField(max_length=255) + action = models.CharField(max_length=10, choices=ACTION_CHOICES) + timestamp = models.DateTimeField(auto_now_add=True) + + def _str_(self): + return f"{self.timestamp} - {self.user} - {self.action} - {self.file_name}" \ No newline at end of file diff --git a/backend/chat/serializers.py b/backend/chat/serializers.py index 0c721c061..2bc8ec1e8 100644 --- a/backend/chat/serializers.py +++ b/backend/chat/serializers.py @@ -1,8 +1,12 @@ from django.core.exceptions import ValidationError from django.utils import timezone from rest_framework import serializers +import hashlib +from rest_framework import serializers +from rest_framework import serializers + -from chat.models import Conversation, Message, Role, Version +from chat.models import Conversation, Message, Role, Version , FileUpload def should_serialize(validated_data, field_name) -> bool: @@ -150,3 +154,39 @@ def update(self, instance, validated_data): version_serializer.save(conversation=instance) return instance + + +#task-3 step-8 + +class ConversationSummarySerializer(serializers.ModelSerializer): + class Meta: + model = Conversation + fields = ['id', 'user', 'summary', 'created_at'] + + +#task-3 step-9 +class FileUploadSerializer(serializers.ModelSerializer): + class Meta: + model = FileUpload + fields = ['id', 'file', 'original_name', 'uploaded_at'] + + def validate(self, data): + file = data.get('file') + sha256 = hashlib.sha256() + for chunk in file.chunks(): + sha256.update(chunk) + file_hash = sha256.hexdigest() + + if FileUpload.objects.filter(file_hash=file_hash).exists(): + raise serializers.ValidationError("This file has already been uploaded.") + + data['file_hash'] = file_hash + data['original_name'] = file.name + return data + +# task-3 step-10 + +class FileUploadListSerializer(serializers.ModelSerializer): + class Meta: + model = FileUpload + fields = ['id', 'original_name', 'file', 'uploaded_at', 'file_hash'] diff --git a/backend/chat/signals.py b/backend/chat/signals.py new file mode 100644 index 000000000..12cde4e36 --- /dev/null +++ b/backend/chat/signals.py @@ -0,0 +1,27 @@ +from django.db.models.signals import post_save +from django.dispatch import receiver +from .models import Conversation, Message +import openai + +@receiver(post_save, sender=Conversation) +def generate_summary(sender, instance, created, **kwargs): + if created or not instance.summary: + messages = Message.objects.filter(conversation=instance).order_by('created_at') + text = "\n".join([msg.content for msg in messages]) + + if text.strip(): + try: + # Use OpenAI or a simple logic to summarize + response = openai.ChatCompletion.create( + model="gpt-3.5-turbo", + messages=[ + {"role": "system", "content": "Summarize this conversation briefly."}, + {"role": "user", "content": text[:3000]} # avoid token limit + ] + ) + summary = response.choices[0].message.content.strip() + except Exception as e: + summary = "Summary generation failed." + + instance.summary = summary + instance.save(update_fields=["summary"]) \ No newline at end of file diff --git a/backend/chat/tasks.py b/backend/chat/tasks.py new file mode 100644 index 000000000..867979613 --- /dev/null +++ b/backend/chat/tasks.py @@ -0,0 +1,10 @@ +from celery import shared_task +from django.utils import timezone +from chat.models import Conversation +from datetime import timedelta + +@shared_task +def cleanup_old_conversations(): + cutoff = timezone.now() - timedelta(days=30) + deleted = Conversation.objects.filter(created_at_lt=cutoff, deleted_at_isnull=True).update(deleted_at=timezone.now()) + return f"{deleted} conversations soft-deleted." \ No newline at end of file diff --git a/backend/chat/urls.py b/backend/chat/urls.py index bd8ceadc0..b8284a377 100644 --- a/backend/chat/urls.py +++ b/backend/chat/urls.py @@ -1,6 +1,10 @@ from django.urls import path from chat import views +from .views import ConversationSummaryListAPIView +from .views import FileUploadView +from .views import FileUploadListView +from .views import FileUploadDeleteView urlpatterns = [ path("", views.chat_root_view, name="chat_root_view"), @@ -19,4 +23,18 @@ ), path("conversations//delete/", views.conversation_soft_delete, name="conversation_delete"), path("versions//add_message/", views.version_add_message, name="version_add_message"), + + path('conversation-summaries/', ConversationSummaryListAPIView.as_view(), name='conversation-summaries'), + path('upload/', FileUploadView.as_view(), name='file-upload'), + path('upload/', FileUploadView.as_view(), name='file-upload'), + path('files/', FileUploadListView.as_view(), name='file-list'), + path('upload/', FileUploadView.as_view(), name='file-upload'), + path('files/', FileUploadListView.as_view(), name='file-list'), + path('files//delete/', FileUploadDeleteView.as_view(), name='file-delete'), + + path('delete//', views.delete_uploaded_file, name='delete-file'), + path('conversation//summary/', views.get_conversation_summary, name='conversation-summary'), + +path('conversation//', views.conversation_manage, name='conversation-manage'), ] + diff --git a/backend/chat/views.py b/backend/chat/views.py index 0d18f7a69..77d01f5fc 100644 --- a/backend/chat/views.py +++ b/backend/chat/views.py @@ -1,12 +1,22 @@ from django.contrib.auth.decorators import login_required from django.utils import timezone -from rest_framework import status +from rest_framework import status , generics, filters from rest_framework.decorators import api_view from rest_framework.response import Response +from django.http import HttpResponse -from chat.models import Conversation, Message, Version -from chat.serializers import ConversationSerializer, MessageSerializer, TitleSerializer, VersionSerializer +from chat.models import Conversation, Message, Version ,FileUpload +from chat.serializers import ConversationSerializer, MessageSerializer, TitleSerializer, VersionSerializer,ConversationSummarySerializer,FileUploadSerializer from chat.utils.branching import make_branched_conversation +from django_filters.rest_framework import DjangoFilterBackend + +from rest_framework.views import APIView +from rest_framework.response import Response + +from rest_framework.generics import ListAPIView +from .serializers import FileUploadListSerializer +from rest_framework.generics import DestroyAPIView + @api_view(["GET"]) @@ -147,7 +157,6 @@ def conversation_add_message(request, pk): serializer = MessageSerializer(data=request.data) if serializer.is_valid(): serializer.save(version=version) - # return Response(serializer.data, status=status.HTTP_201_CREATED) return Response( { "message": serializer.data, @@ -230,3 +239,122 @@ def version_add_message(request, pk): status=status.HTTP_201_CREATED, ) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + +# task-3 step-8 +class ConversationSummaryListAPIView(generics.ListAPIView): + queryset = Conversation.objects.filter(deleted_at__isnull=True).order_by('-created_at') + serializer_class = ConversationSummarySerializer + filter_backends = [DjangoFilterBackend, filters.SearchFilter] + filterset_fields = ['user__username'] + search_fields = ['summary'] + + + +# task-3 step-9 + +class FileUploadView(APIView): + def post(self, request): + serializer = FileUploadSerializer(data=request.data) + if serializer.is_valid(): + serializer.save() + return Response({"message": "File uploaded successfully.", "data": serializer.data}) + else: + return HttpResponse("Duplicate Files Found!!!") + + + + +# task-3 step-10 + +class FileUploadListView(ListAPIView): + queryset = FileUpload.objects.all().order_by('-uploaded_at') + serializer_class = FileUploadListSerializer + + + +#task-3 step-11 + +class FileUploadDeleteView(DestroyAPIView): + queryset = FileUpload.objects.all() + serializer_class = FileUploadListSerializer + lookup_field = 'id' + + +# task -4 + +from rest_framework.decorators import api_view, permission_classes +from rest_framework.permissions import IsAuthenticated +from rest_framework.response import Response +from rest_framework import status +from .models import FileUpload +from .serializers import FileUploadSerializer + +import logging +from .models import FileLog + +logger = logging.getLogger(__name__) + +@api_view(['POST']) +@permission_classes([IsAuthenticated]) +def upload_file_view(request): + if not request.user.is_staff: + return Response({'detail': 'Permission denied. Admins only.'}, status=403) + + if 'file' not in request.FILES: + return Response({'error': 'No file provided.'}, status=400) + + file_obj = request.FILES['file'] + uploaded_file = FileUpload.objects.create(user=request.user, file=file_obj) + + # ✅ Log to console + logger.info(f"{request.user.email} uploaded file: {file_obj.name}") + + # ✅ Log to DB + FileLog.objects.create(user=request.user, file_name=file_obj.name, action='UPLOAD') + + return Response({'message': 'File uploaded successfully.'}, status=201) + +@api_view(['DELETE']) +@permission_classes([IsAuthenticated]) +def delete_uploaded_file(request, file_id): + try: + uploaded_file = FileUpload.objects.get(id=file_id) + file_name = uploaded_file.file.name + uploaded_file.delete() + + # ✅ Create a FileLog entry + FileLog.objects.create( + user=request.user, + action='DELETE', + file_name=file_name + ) + + logger.info(f"{request.user.email} deleted file: {file_name}") + return Response({'message': 'File deleted successfully.'}, status=200) + except FileUpload.DoesNotExist: + return Response({'error': 'File not found.'}, status=404) + + +from django.core.cache import cache +from rest_framework.decorators import api_view, permission_classes +from rest_framework.permissions import IsAuthenticated +from rest_framework.response import Response +from chat.models import Conversation + +@api_view(['GET']) +@permission_classes([IsAuthenticated]) +def get_conversation_summary(request, id): + cache_key = f"conversation_summary_{id}" + summary = cache.get(cache_key) + + if summary is None: + try: + conversation = Conversation.objects.get(id=id, user=request.user) + except Conversation.DoesNotExist: + return Response({"error": "Conversation not found"}, status=404) + + summary = conversation.summary or "No summary available" + cache.set(cache_key, summary, timeout=3600) # cache for 1 hour + + return Response({"id": id, "summary": summary}) \ No newline at end of file diff --git a/backend/uploads/Prime_Programs_Python.docx b/backend/uploads/Prime_Programs_Python.docx new file mode 100644 index 000000000..ccef82676 Binary files /dev/null and b/backend/uploads/Prime_Programs_Python.docx differ diff --git a/backend/uploads/Prime_Programs_Python_6AIR8jH.docx b/backend/uploads/Prime_Programs_Python_6AIR8jH.docx new file mode 100644 index 000000000..ccef82676 Binary files /dev/null and b/backend/uploads/Prime_Programs_Python_6AIR8jH.docx differ diff --git a/backend/uploads/Prime_Programs_Python_KIWqaEv.docx b/backend/uploads/Prime_Programs_Python_KIWqaEv.docx new file mode 100644 index 000000000..ccef82676 Binary files /dev/null and b/backend/uploads/Prime_Programs_Python_KIWqaEv.docx differ diff --git a/backend/uploads/Prime_Programs_Python_ZB0KpFl.docx b/backend/uploads/Prime_Programs_Python_ZB0KpFl.docx new file mode 100644 index 000000000..ccef82676 Binary files /dev/null and b/backend/uploads/Prime_Programs_Python_ZB0KpFl.docx differ diff --git a/backend/uploads/Prime_Programs_Python_cks7EQh.docx b/backend/uploads/Prime_Programs_Python_cks7EQh.docx new file mode 100644 index 000000000..ccef82676 Binary files /dev/null and b/backend/uploads/Prime_Programs_Python_cks7EQh.docx differ diff --git a/backend/uploads/Prime_Programs_Python_gpOoYio.docx b/backend/uploads/Prime_Programs_Python_gpOoYio.docx new file mode 100644 index 000000000..ccef82676 Binary files /dev/null and b/backend/uploads/Prime_Programs_Python_gpOoYio.docx differ diff --git a/backend/uploads/Prime_Programs_Python_lXvP7SJ.docx b/backend/uploads/Prime_Programs_Python_lXvP7SJ.docx new file mode 100644 index 000000000..ccef82676 Binary files /dev/null and b/backend/uploads/Prime_Programs_Python_lXvP7SJ.docx differ diff --git a/backend/uploads/Prime_Programs_Python_nDf9jbf.docx b/backend/uploads/Prime_Programs_Python_nDf9jbf.docx new file mode 100644 index 000000000..ccef82676 Binary files /dev/null and b/backend/uploads/Prime_Programs_Python_nDf9jbf.docx differ diff --git a/backend/uploads/Prime_Programs_Python_v94A38w.docx b/backend/uploads/Prime_Programs_Python_v94A38w.docx new file mode 100644 index 000000000..ccef82676 Binary files /dev/null and b/backend/uploads/Prime_Programs_Python_v94A38w.docx differ diff --git a/backend/uploads/Prime_Programs_Python_xwAPtWt.docx b/backend/uploads/Prime_Programs_Python_xwAPtWt.docx new file mode 100644 index 000000000..ccef82676 Binary files /dev/null and b/backend/uploads/Prime_Programs_Python_xwAPtWt.docx differ diff --git a/backend/uploads/Python_Interview_Coding_Questions.pdf b/backend/uploads/Python_Interview_Coding_Questions.pdf new file mode 100644 index 000000000..87d3a3291 Binary files /dev/null and b/backend/uploads/Python_Interview_Coding_Questions.pdf differ diff --git a/backend/uploads/Python_Interview_Coding_Questions_HWPDd8R.pdf b/backend/uploads/Python_Interview_Coding_Questions_HWPDd8R.pdf new file mode 100644 index 000000000..87d3a3291 Binary files /dev/null and b/backend/uploads/Python_Interview_Coding_Questions_HWPDd8R.pdf differ diff --git a/backend/uploads/Python_Interview_Coding_Questions_WltsXkR.pdf b/backend/uploads/Python_Interview_Coding_Questions_WltsXkR.pdf new file mode 100644 index 000000000..87d3a3291 Binary files /dev/null and b/backend/uploads/Python_Interview_Coding_Questions_WltsXkR.pdf differ diff --git a/backend/uploads/Python_Interview_Coding_Questions_vxgOYSC.pdf b/backend/uploads/Python_Interview_Coding_Questions_vxgOYSC.pdf new file mode 100644 index 000000000..87d3a3291 Binary files /dev/null and b/backend/uploads/Python_Interview_Coding_Questions_vxgOYSC.pdf differ diff --git a/backend/uploads/Python_Interview_Questions_1.pdf b/backend/uploads/Python_Interview_Questions_1.pdf new file mode 100644 index 000000000..79d8ba396 Binary files /dev/null and b/backend/uploads/Python_Interview_Questions_1.pdf differ diff --git a/backend/uploads/receipt.pdf b/backend/uploads/receipt.pdf new file mode 100644 index 000000000..c1f2b5b88 Binary files /dev/null and b/backend/uploads/receipt.pdf differ diff --git a/backend/uploads/receipt_12.pdf b/backend/uploads/receipt_12.pdf new file mode 100644 index 000000000..1a025ed6e Binary files /dev/null and b/backend/uploads/receipt_12.pdf differ diff --git a/backend/uploads/receipt_12_hCMv6oC.pdf b/backend/uploads/receipt_12_hCMv6oC.pdf new file mode 100644 index 000000000..1a025ed6e Binary files /dev/null and b/backend/uploads/receipt_12_hCMv6oC.pdf differ