Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions backend/backend/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from .celery import app as celery_app

__all__ = ('celery_app',)
14 changes: 14 additions & 0 deletions backend/backend/celery.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
from __future__ import absolute_import, unicode_literals
import os
from celery import Celery

# Set default Django settings module
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'backend.settings')

app = Celery('backend')

# Load settings from Django settings with CELERY namespace
app.config_from_object('django.conf:settings', namespace='CELERY')

# Auto-discover tasks in all installed apps
app.autodiscover_tasks()
74 changes: 67 additions & 7 deletions backend/backend/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,14 @@
"""

import os
import logging

from pathlib import Path

from dotenv import load_dotenv

from celery.schedules import crontab

load_dotenv()

# Build paths inside the project like this: BASE_DIR / 'subdir'.
Expand All @@ -30,7 +34,7 @@
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True

ALLOWED_HOSTS = []
ALLOWED_HOSTS = ["127.0.0.1", "localhost"]

# Application definition

Expand All @@ -47,8 +51,48 @@
"authentication",
"chat",
"gpt",
'django_crontab',
]

LOG_DIR = BASE_DIR / "logs"
LOG_DIR.mkdir(exist_ok=True)

LOGGING = {
"version": 1,
"disable_existing_loggers": False,
"formatters": {
"json": {
"format": '{"time":"%(asctime)s", "level":"%(levelname)s", "message":"%(message)s"}',
},
},
"handlers": {
"file_activity": {
"class": "logging.handlers.RotatingFileHandler",
"filename": str(LOG_DIR / "activity.log"),
"maxBytes": 5 * 1024 * 1024,
"backupCount": 3,
"encoding": "utf-8",
"formatter": "json",
},
},
"loggers": {
"activity": {
"handlers": ["file_activity"],
"level": "INFO",
"propagate": False,
},
},
}


CACHES = {
"default": {
"BACKEND": "django.core.cache.backends.locmem.LocMemCache",
"LOCATION": "local-cache",
"TIMEOUT": 300, # default TTL (seconds)
}
}

MIDDLEWARE = [
"django.middleware.security.SecurityMiddleware",
"django.contrib.sessions.middleware.SessionMiddleware",
Expand Down Expand Up @@ -86,11 +130,23 @@

DATABASES = {
"default": {
"ENGINE": "django.db.backends.sqlite3",
"NAME": BASE_DIR / "db.sqlite3",
"ENGINE": "django.db.backends.postgresql",
"NAME": os.environ.get("POSTGRES_DB"),
"USER": os.environ.get("POSTGRES_USER"),
"PASSWORD": os.environ.get("POSTGRES_PASSWORD"),
"HOST": os.environ.get("POSTGRES_HOST", "127.0.0.1"),
"PORT": os.environ.get("POSTGRES_PORT", "5432"),
}
}

CELERY_BROKER_URL = 'redis://localhost:6379/0'
CELERY_BEAT_SCHEDULE = {
'cleanup-every-day': {
'task': 'chat.tasks.cleanup_old_conversations_task',
'schedule': crontab(hour=0, minute=0), # every day at midnight
},
}

# Password validation
# https://docs.djangoproject.com/en/4.2/ref/settings/#auth-password-validators

Expand Down Expand Up @@ -138,14 +194,18 @@
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"

CORS_ALLOWED_ORIGINS = [
FRONTEND_URL,
"http://localhost:3000",
]
CORS_ALLOW_CREDENTIALS = True

CSRF_TRUSTED_ORIGINS = [
FRONTEND_URL,
"http://localhost:3000",
]

CORNJOBS = [
('0 0 * * *', 'django.core.management.call_command', ['cleanup_conversations']),
]

SESSION_COOKIE_SECURE = True
SESSION_COOKIE_SECURE = False
CSRF_COOKIE_SECURE = True
CSRF_COOKIE_SAMESITE = "None"
CSRF_COOKIE_SAMESITE = 'Lax'
4 changes: 4 additions & 0 deletions backend/celerybeat-schedule.bak
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
'entries', (0, 405)
'__version__', (512, 20)
'tz', (1024, 28)
'utc_enabled', (1536, 4)
Binary file added backend/celerybeat-schedule.dat
Binary file not shown.
4 changes: 4 additions & 0 deletions backend/celerybeat-schedule.dir
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
'entries', (0, 405)
'__version__', (512, 20)
'tz', (1024, 28)
'utc_enabled', (1536, 4)
11 changes: 9 additions & 2 deletions backend/chat/admin.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,15 @@
from django.contrib import admin
from django.utils import timezone
from nested_admin.nested import NestedModelAdmin, NestedStackedInline, NestedTabularInline
from .models import Conversation, Message, Role, Version, UploadedFile

from chat.models import Conversation, Message, Role, Version

@admin.register(UploadedFile)
class UploadedFileAdmin(admin.ModelAdmin):
list_display = ["id", "file", "uploaded_at", "checksum", "user"]
list_filter = ["uploaded_at", "user"]
search_fields = ["file", "user__username"]
readonly_fields = ["checksum"]


class RoleAdmin(NestedModelAdmin):
Expand Down Expand Up @@ -51,7 +58,7 @@ 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","summary","status", "created_at", "modified_at", "deleted_at", "version_count", "is_deleted", "user")
list_filter = (DeletedListFilter,)
ordering = ("-modified_at",)

Expand Down
14 changes: 14 additions & 0 deletions backend/chat/management/commands/cleanup_conversations.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
from django.core.management.base import BaseCommand
from chat.models import Conversation
from django.utils import timezone
from datetime import timedelta

class Command(BaseCommand):
help = 'Deletes conversations older than 30 days'

def handle(self, *args, **kwargs):
cutoff_date = timezone.now() - timedelta(days=30)
old_conversations = Conversation.objects.filter(created_at__lt=cutoff_date)
count = old_conversations.count()
old_conversations.delete()
self.stdout.write(self.style.SUCCESS(f'Deleted {count} old conversations'))
17 changes: 17 additions & 0 deletions backend/chat/migrations/0002_conversation_status.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Generated by Django 5.0.2 on 2025-10-07 08:20

from django.db import migrations, models


class Migration(migrations.Migration):
dependencies = [
("chat", "0001_initial"),
]

operations = [
migrations.AddField(
model_name="conversation",
name="status",
field=models.CharField(default="active", max_length=20),
),
]
17 changes: 17 additions & 0 deletions backend/chat/migrations/0003_conversation_summary.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Generated by Django 5.0.2 on 2025-10-07 10:01

from django.db import migrations, models


class Migration(migrations.Migration):
dependencies = [
("chat", "0002_conversation_status"),
]

operations = [
migrations.AddField(
model_name="conversation",
name="summary",
field=models.TextField(blank=True, null=True),
),
]
26 changes: 26 additions & 0 deletions backend/chat/migrations/0004_uploadedfile.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# Generated by Django 5.0.2 on 2025-10-07 18:33

import django.db.models.deletion
import uuid
from django.conf import settings
from django.db import migrations, models


class Migration(migrations.Migration):
dependencies = [
("chat", "0003_conversation_summary"),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]

operations = [
migrations.CreateModel(
name="UploadedFile",
fields=[
("id", models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
("file", models.FileField(upload_to="uploads/")),
("uploaded_at", models.DateTimeField(auto_now_add=True)),
("checksum", models.CharField(max_length=64, unique=True)),
("user", models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
),
]
17 changes: 17 additions & 0 deletions backend/chat/migrations/0005_uploadedfile_extracted_text.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Generated by Django 5.0.2 on 2025-10-08 11:19

from django.db import migrations, models


class Migration(migrations.Migration):
dependencies = [
("chat", "0004_uploadedfile"),
]

operations = [
migrations.AddField(
model_name="uploadedfile",
name="extracted_text",
field=models.TextField(blank=True, null=True),
),
]
50 changes: 48 additions & 2 deletions backend/chat/models.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import uuid

import io
from django.db import models

from authentication.models import CustomUser
Expand All @@ -22,6 +22,8 @@ class Conversation(models.Model):
)
deleted_at = models.DateTimeField(null=True, blank=True)
user = models.ForeignKey(CustomUser, on_delete=models.CASCADE)
status = models.CharField(max_length=20, blank=False, null=False, default="active")
summary = models.TextField(blank=True, null=True)

def __str__(self):
return self.title
Expand Down Expand Up @@ -58,8 +60,52 @@ class Meta:
ordering = ["created_at"]

def save(self, *args, **kwargs):
self.version.conversation.save()
super().save(*args, **kwargs)

messages = self.version.messages.all()
summary_text = "".join([message.content for message in messages])[:200]
self.version.conversation.summary = summary_text
self.version.conversation.save()

def __str__(self):
return f"{self.role}: {self.content[:20]}..."



import hashlib
from django.core.exceptions import ValidationError


class UploadedFile(models.Model):
"""
Model to store uploaded files with a checksum for duplication check.
"""
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
user = models.ForeignKey(CustomUser, on_delete=models.CASCADE)
file = models.FileField(upload_to="uploads/")
uploaded_at = models.DateTimeField(auto_now_add=True)
checksum = models.CharField(max_length=64, unique=True)
extracted_text = models.TextField(null=True, blank=True)

def __str__(self):
return f"{self.file.name}"

def clean(self):
# Calculate checksum for duplication validation
if self.file:
sha = hashlib.sha256()
for chunk in self.file.chunks():
sha.update(chunk)
checksum = sha.hexdigest()
if UploadedFile.objects.filter(checksum=checksum).exists():
raise ValidationError("This file already exists.")
self.checksum = checksum

def save(self, *args, **kwargs):
if not self.checksum and self.file:
sha = hashlib.sha256()
for chunk in self.file.chunks():
sha.update(chunk)
self.checksum = sha.hexdigest()
super().save(*args, **kwargs)

13 changes: 11 additions & 2 deletions backend/chat/serializers.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
from chat.models import Conversation, Message, Role, Version, UploadedFile
from django.core.exceptions import ValidationError
from django.utils import timezone
from rest_framework import serializers

from chat.models import Conversation, Message, Role, Version


def should_serialize(validated_data, field_name) -> bool:
if validated_data.get(field_name) is not None:
Expand Down Expand Up @@ -42,6 +41,16 @@ def to_representation(self, instance):
return representation



class UploadedFileSerializer(serializers.ModelSerializer):
"""Serializer for file uploads with metadata."""

class Meta:
model = UploadedFile
fields = ['id', 'file', 'uploaded_at', 'checksum']
read_only_fields = ['id', 'uploaded_at', 'checksum']


class VersionSerializer(serializers.ModelSerializer):
messages = MessageSerializer(many=True)
active = serializers.SerializerMethodField()
Expand Down
7 changes: 7 additions & 0 deletions backend/chat/tasks.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
from celery import shared_task
from django.core.management import call_command

@shared_task
def cleanup_old_conversations_task():
# Calls your management command
call_command('cleanup_conversations')
Loading