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',)
27 changes: 27 additions & 0 deletions backend/backend/celery.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import os
from celery import Celery
from celery.schedules import crontab

os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'backend.settings')


app = Celery('backend')
app.config_from_object('django.conf:settings', namespace='CELERY')
app.autodiscover_tasks()

# Celery Beat Schedule
app.conf.beat_schedule = {
'cleanup-old-conversations': {
'task': 'chat.tasks.cleanup_old_conversations',
'schedule': crontab(hour=2, minute=0), # Daily at 2 AM
'args': (30,) # Delete conversations older than 30 days
},
'generate-missing-summaries': {
'task': 'chat.tasks.generate_missing_summaries',
'schedule': crontab(hour=3, minute=0), # Daily at 3 AM
},
}

@app.task(bind=True)
def debug_task(self):
print(f'Request: {self.request!r}')
28 changes: 22 additions & 6 deletions backend/backend/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,17 +15,19 @@

from dotenv import load_dotenv

load_dotenv()


# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent
load_dotenv(os.path.join(BASE_DIR, ".env"))

# 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"]
SECRET_KEY = os.environ.get("DJANGO_SECRET_KEY", "fallback-secret-key")
print("SECRET:", os.environ.get("DJANGO_SECRET_KEY"))
FRONTEND_URL = os.environ.get("FRONTEND_URL", "http://localhost:3000")

# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True
Expand Down Expand Up @@ -85,9 +87,13 @@
# 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': 'chatgpt_db',
'USER': 'chatgpt_user',
'PASSWORD': 'chatgpt_password',
'HOST': 'localhost',
'PORT': '5432',
}
}

Expand Down Expand Up @@ -149,3 +155,13 @@
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True
CSRF_COOKIE_SAMESITE = "None"

# Celery Configuration
CELERY_BROKER_URL = os.getenv('CELERY_BROKER_URL', 'redis://localhost:6379/0')
CELERY_RESULT_BACKEND = os.getenv('CELERY_RESULT_BACKEND', 'redis://localhost:6379/0')
CELERY_ACCEPT_CONTENT = ['json']
CELERY_TASK_SERIALIZER = 'json'
CELERY_RESULT_SERIALIZER = 'json'
CELERY_TIMEZONE = 'UTC'
CELERY_TASK_TRACK_STARTED = True
CELERY_TASK_TIME_LIMIT = 30 * 60 # 30 minutes
28 changes: 24 additions & 4 deletions backend/chat/admin.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from django.contrib import admin
from django.utils import timezone
from nested_admin.nested import NestedModelAdmin, NestedStackedInline, NestedTabularInline

from django.utils.html import format_html
from chat.models import Conversation, Message, Role, Version


Expand Down Expand Up @@ -51,8 +51,8 @@ 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_filter = (DeletedListFilter,)
list_display = ("title", "id", "created_at", "modified_at", "deleted_at", "version_count", "is_deleted", "has_summary", "summary_status", "user")
list_filter = (DeletedListFilter,"is_summary_stale",)
ordering = ("-modified_at",)

def undelete_selected(self, request, queryset):
Expand All @@ -79,7 +79,27 @@ def is_deleted(self, obj):

is_deleted.boolean = True
is_deleted.short_description = "Deleted?"

def has_summary(self, obj):
"""Display if conversation has summary"""
if obj.summary:
return format_html(
'<span style="color: green;">✓ Has Summary</span>'
)
return format_html(
'<span style="color: red;">✗ No Summary</span>'
)
has_summary.short_description = "Summary Status"

def summary_status(self, obj):
"""Show if summary is stale"""
if obj.is_summary_stale:
return format_html(
'<span style="color: orange;">Stale</span>'
)
return format_html(
'<span style="color: green;">Current</span>'
)
summary_status.short_description = "Summary Freshness"

class VersionAdmin(NestedModelAdmin):
inlines = [MessageInline]
Expand Down
3 changes: 3 additions & 0 deletions backend/chat/apps.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,6 @@
class ChatConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "chat"

def ready(self):
import chat.signals
68 changes: 68 additions & 0 deletions backend/chat/management/commands/cleanup_conversations.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
from django.core.management.base import BaseCommand
from django.utils.timezone import now
from django.db.models import Count
from datetime import timedelta
from chat.models import Conversation
import logging

logger = logging.getLogger(__name__)

class Command(BaseCommand):
help = 'Clean up old conversations based on age'

def add_arguments(self, parser):
parser.add_argument(
'--days',
type=int,
default=30,
help='Delete conversations older than specified days (default: 30)'
)
parser.add_argument(
'--dry-run',
action='store_true',
help='Show what would be deleted without actually deleting'
)
parser.add_argument(
'--user',
type=str,
help='Only delete conversations for specific user'
)

def handle(self, *args, **options):
days = options['days']
dry_run = options['dry_run']
user_filter = options.get('user')

cutoff_date = now() - timedelta(days=days)
queryset = Conversation.objects.filter(created_at__lt=cutoff_date)

if user_filter:
queryset = queryset.filter(user__username=user_filter)

count = queryset.count()

if count == 0:
self.stdout.write(
self.style.WARNING('No conversations found matching criteria')
)
return

self.stdout.write(f"Conversations to delete: {count}\n")

if dry_run:
self.stdout.write(self.style.WARNING('DRY RUN - No deletions made\n'))
for conv in queryset[:5]:
self.stdout.write(
f" - {conv.title} ({conv.messages.count()} messages)"
)
return

confirm = input(f"Delete {count} conversations? (yes/no): ")

if confirm.lower() == 'yes':
deleted_count = queryset.delete()[0]
self.stdout.write(
self.style.SUCCESS(f'✓ Deleted {deleted_count} conversations')
)
else:
self.stdout.write(self.style.WARNING('Deletion cancelled'))
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
# Generated by Django 5.2.11 on 2026-02-28 06:51

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


class Migration(migrations.Migration):

dependencies = [
('chat', '0001_initial'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]

operations = [
migrations.CreateModel(
name='ActivityLog',
fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('action', models.CharField(choices=[('file_upload', 'File Upload'), ('file_delete', 'File Delete'), ('file_access', 'File Access'), ('conversation_create', 'Conversation Create'), ('conversation_delete', 'Conversation Delete'), ('conversation_edit', 'Conversation Edit'), ('message_send', 'Message Send'), ('summary_generate', 'Summary Generate'), ('summary_regenerate', 'Summary Regenerate')], max_length=50)),
('resource_type', models.CharField(max_length=50)),
('resource_id', models.CharField(blank=True, max_length=100, null=True)),
('details', models.JSONField(blank=True, default=dict)),
('ip_address', models.GenericIPAddressField(blank=True, null=True)),
('user_agent', models.TextField(blank=True)),
('status', models.CharField(choices=[('success', 'Success'), ('failed', 'Failed')], default='success', max_length=20)),
('timestamp', models.DateTimeField(auto_now_add=True, db_index=True)),
],
options={
'ordering': ['-timestamp'],
},
),
migrations.CreateModel(
name='FilePermission',
fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('permission', models.CharField(choices=[('view', 'View'), ('upload', 'Upload'), ('delete', 'Delete'), ('share', 'Share')], max_length=20)),
('created_at', models.DateTimeField(auto_now_add=True)),
],
),
migrations.CreateModel(
name='UploadedFile',
fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('file', models.FileField(upload_to='uploads/%Y/%m/%d/')),
('filename', models.CharField(max_length=255)),
('file_size', models.BigIntegerField()),
('file_type', models.CharField(max_length=50)),
('file_hash', models.CharField(max_length=64, unique=True)),
('status', models.CharField(choices=[('pending', 'Pending'), ('processing', 'Processing'), ('completed', 'Completed'), ('failed', 'Failed')], default='pending', max_length=20)),
('uploaded_at', models.DateTimeField(auto_now_add=True)),
('processed_at', models.DateTimeField(blank=True, null=True)),
('error_message', models.TextField(blank=True, null=True)),
('mime_type', models.CharField(blank=True, max_length=100, null=True)),
('page_count', models.IntegerField(blank=True, null=True)),
('is_indexed', models.BooleanField(default=False)),
],
options={
'ordering': ['-uploaded_at'],
},
),
migrations.CreateModel(
name='UserRole',
fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('role', models.CharField(choices=[('user', 'Regular User'), ('moderator', 'Moderator'), ('admin', 'Administrator')], default='user', max_length=20)),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
],
),
migrations.AlterModelOptions(
name='conversation',
options={'ordering': ['-created_at']},
),
migrations.AddField(
model_name='conversation',
name='is_summary_stale',
field=models.BooleanField(default=False, help_text='Indicates if summary needs to be regenerated'),
),
migrations.AddField(
model_name='conversation',
name='summary',
field=models.TextField(blank=True, help_text='Auto-generated summary of the conversation', null=True),
),
migrations.AddField(
model_name='conversation',
name='summary_generated_at',
field=models.DateTimeField(blank=True, help_text='Timestamp when summary was generated', null=True),
),
migrations.AddIndex(
model_name='conversation',
index=models.Index(fields=['user', '-created_at'], name='chat_conver_user_id_49b34c_idx'),
),
migrations.AddIndex(
model_name='conversation',
index=models.Index(fields=['is_summary_stale'], name='chat_conver_is_summ_c1de9d_idx'),
),
migrations.AddField(
model_name='activitylog',
name='user',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='activity_logs', to=settings.AUTH_USER_MODEL),
),
migrations.AddField(
model_name='filepermission',
name='granted_by',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='granted_permissions', to=settings.AUTH_USER_MODEL),
),
migrations.AddField(
model_name='filepermission',
name='user',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='file_permissions', to=settings.AUTH_USER_MODEL),
),
migrations.AddField(
model_name='uploadedfile',
name='conversation',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='files', to='chat.conversation'),
),
migrations.AddField(
model_name='uploadedfile',
name='user',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='uploaded_files', to=settings.AUTH_USER_MODEL),
),
migrations.AddField(
model_name='filepermission',
name='file',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='permissions', to='chat.uploadedfile'),
),
migrations.AddField(
model_name='userrole',
name='user',
field=models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='role_profile', to=settings.AUTH_USER_MODEL),
),
migrations.AddIndex(
model_name='activitylog',
index=models.Index(fields=['user', '-timestamp'], name='chat_activi_user_id_a7ffef_idx'),
),
migrations.AddIndex(
model_name='activitylog',
index=models.Index(fields=['action', '-timestamp'], name='chat_activi_action_c91d2c_idx'),
),
migrations.AddIndex(
model_name='activitylog',
index=models.Index(fields=['resource_type', 'resource_id'], name='chat_activi_resourc_9d4685_idx'),
),
migrations.AddIndex(
model_name='uploadedfile',
index=models.Index(fields=['user', '-uploaded_at'], name='chat_upload_user_id_618957_idx'),
),
migrations.AddIndex(
model_name='uploadedfile',
index=models.Index(fields=['file_hash'], name='chat_upload_file_ha_43c6d5_idx'),
),
migrations.AddIndex(
model_name='uploadedfile',
index=models.Index(fields=['status'], name='chat_upload_status_97172d_idx'),
),
migrations.AlterUniqueTogether(
name='filepermission',
unique_together={('user', 'file', 'permission')},
),
]
Loading