mirror of
https://github.com/thecyberlearn/quantum-ai-v2.git
synced 2026-08-18 15:13:00 +00:00
⏰ Implement chat session timeout system
- Add expires_at field to ChatSession model with 2-hour default - Sessions automatically expire after 2 hours of inactivity - Active conversations extend session by 2 hours per message - Add session expiration checks in chat views - Create cleanup management command for expired sessions - Add session status indicators in chat UI - Fix null safety for existing sessions without expiration 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
ab2360a91d
commit
d750857e4e
30
agents/management/commands/cleanup_expired_sessions.py
Normal file
30
agents/management/commands/cleanup_expired_sessions.py
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
from django.core.management.base import BaseCommand
|
||||||
|
from django.utils import timezone
|
||||||
|
from agents.models import ChatSession
|
||||||
|
|
||||||
|
class Command(BaseCommand):
|
||||||
|
help = 'Mark expired chat sessions as expired'
|
||||||
|
|
||||||
|
def handle(self, *args, **options):
|
||||||
|
now = timezone.now()
|
||||||
|
|
||||||
|
# Find active sessions that have expired
|
||||||
|
expired_sessions = ChatSession.objects.filter(
|
||||||
|
status='active',
|
||||||
|
expires_at__lt=now
|
||||||
|
)
|
||||||
|
|
||||||
|
count = expired_sessions.count()
|
||||||
|
|
||||||
|
if count > 0:
|
||||||
|
# Mark them as expired
|
||||||
|
expired_sessions.update(
|
||||||
|
status='expired',
|
||||||
|
completed_at=now
|
||||||
|
)
|
||||||
|
|
||||||
|
self.stdout.write(
|
||||||
|
self.style.SUCCESS(f'✅ Marked {count} expired sessions as expired')
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
self.stdout.write('✅ No expired sessions found')
|
||||||
@ -0,0 +1,40 @@
|
|||||||
|
# Generated by Django 5.2.4 on 2025-08-01 10:08
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
(
|
||||||
|
"agents",
|
||||||
|
"0002_agent_agent_type_alter_agent_form_schema_chatsession_and_more",
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="chatsession",
|
||||||
|
name="expires_at",
|
||||||
|
field=models.DateTimeField(
|
||||||
|
blank=True,
|
||||||
|
help_text="Session expiration time (2 hours from last activity)",
|
||||||
|
null=True,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="chatsession",
|
||||||
|
name="status",
|
||||||
|
field=models.CharField(
|
||||||
|
choices=[
|
||||||
|
("active", "Active"),
|
||||||
|
("completed", "Completed"),
|
||||||
|
("expired", "Expired"),
|
||||||
|
("abandoned", "Abandoned"),
|
||||||
|
("failed", "Failed"),
|
||||||
|
],
|
||||||
|
default="active",
|
||||||
|
max_length=20,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
||||||
@ -73,6 +73,7 @@ class ChatSession(models.Model):
|
|||||||
STATUS_CHOICES = [
|
STATUS_CHOICES = [
|
||||||
('active', 'Active'),
|
('active', 'Active'),
|
||||||
('completed', 'Completed'),
|
('completed', 'Completed'),
|
||||||
|
('expired', 'Expired'),
|
||||||
('abandoned', 'Abandoned'),
|
('abandoned', 'Abandoned'),
|
||||||
('failed', 'Failed'),
|
('failed', 'Failed'),
|
||||||
]
|
]
|
||||||
@ -84,10 +85,19 @@ class ChatSession(models.Model):
|
|||||||
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='active')
|
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='active')
|
||||||
context_data = models.JSONField(default=dict, help_text="Session context and progress tracking")
|
context_data = models.JSONField(default=dict, help_text="Session context and progress tracking")
|
||||||
fee_charged = models.DecimalField(max_digits=10, decimal_places=2)
|
fee_charged = models.DecimalField(max_digits=10, decimal_places=2)
|
||||||
|
expires_at = models.DateTimeField(null=True, blank=True, help_text="Session expiration time (2 hours from last activity)")
|
||||||
created_at = models.DateTimeField(auto_now_add=True)
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
updated_at = models.DateTimeField(auto_now=True)
|
updated_at = models.DateTimeField(auto_now=True)
|
||||||
completed_at = models.DateTimeField(null=True, blank=True)
|
completed_at = models.DateTimeField(null=True, blank=True)
|
||||||
|
|
||||||
|
def save(self, *args, **kwargs):
|
||||||
|
# Set expires_at to 2 hours from now if not set
|
||||||
|
if not self.expires_at:
|
||||||
|
from django.utils import timezone
|
||||||
|
from datetime import timedelta
|
||||||
|
self.expires_at = timezone.now() + timedelta(hours=2)
|
||||||
|
super().save(*args, **kwargs)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
ordering = ['-created_at']
|
ordering = ['-created_at']
|
||||||
indexes = [
|
indexes = [
|
||||||
@ -98,6 +108,20 @@ class ChatSession(models.Model):
|
|||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f"{self.agent.name} - {self.user.email} - {self.session_id}"
|
return f"{self.agent.name} - {self.user.email} - {self.session_id}"
|
||||||
|
|
||||||
|
def is_expired(self):
|
||||||
|
from django.utils import timezone
|
||||||
|
if not self.expires_at:
|
||||||
|
return False # Sessions without expiration date are considered active
|
||||||
|
return timezone.now() > self.expires_at
|
||||||
|
|
||||||
|
def extend_session(self):
|
||||||
|
"""Extend session by 2 hours from now"""
|
||||||
|
from django.utils import timezone
|
||||||
|
from datetime import timedelta
|
||||||
|
self.expires_at = timezone.now() + timedelta(hours=2)
|
||||||
|
self.updated_at = timezone.now()
|
||||||
|
self.save()
|
||||||
|
|
||||||
class ChatMessage(models.Model):
|
class ChatMessage(models.Model):
|
||||||
MESSAGE_TYPE_CHOICES = [
|
MESSAGE_TYPE_CHOICES = [
|
||||||
('user', 'User Message'),
|
('user', 'User Message'),
|
||||||
|
|||||||
@ -13,6 +13,71 @@
|
|||||||
padding: 20px;
|
padding: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Enhanced message formatting */
|
||||||
|
.message-bubble h1,
|
||||||
|
.message-bubble h2,
|
||||||
|
.message-bubble h3,
|
||||||
|
.message-bubble h4 {
|
||||||
|
margin: 16px 0 8px 0;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 1.3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-bubble h1 { font-size: 18px; color: var(--primary); }
|
||||||
|
.message-bubble h2 { font-size: 16px; color: var(--primary); }
|
||||||
|
.message-bubble h3 { font-size: 15px; color: var(--on-surface); }
|
||||||
|
.message-bubble h4 { font-size: 14px; color: var(--on-surface); }
|
||||||
|
|
||||||
|
.message-bubble ul,
|
||||||
|
.message-bubble ol {
|
||||||
|
margin: 12px 0;
|
||||||
|
padding-left: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-bubble li {
|
||||||
|
margin: 6px 0;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-bubble strong {
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--on-surface);
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-bubble em {
|
||||||
|
font-style: italic;
|
||||||
|
color: var(--on-surface-variant);
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-bubble p {
|
||||||
|
margin: 10px 0;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-bubble code {
|
||||||
|
background: var(--surface-variant);
|
||||||
|
padding: 2px 6px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-bubble pre {
|
||||||
|
background: var(--surface-variant);
|
||||||
|
padding: 12px;
|
||||||
|
border-radius: 8px;
|
||||||
|
overflow-x: auto;
|
||||||
|
margin: 12px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-bubble blockquote {
|
||||||
|
border-left: 3px solid var(--primary);
|
||||||
|
margin: 12px 0;
|
||||||
|
padding-left: 12px;
|
||||||
|
color: var(--on-surface-variant);
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
.chat-widget {
|
.chat-widget {
|
||||||
background: var(--surface);
|
background: var(--surface);
|
||||||
border: 1px solid var(--outline);
|
border: 1px solid var(--outline);
|
||||||
@ -279,6 +344,11 @@ document.body.setAttribute('data-session-id', '{{ chat_session.session_id }}');
|
|||||||
<div class="session-info">
|
<div class="session-info">
|
||||||
<div class="session-status">
|
<div class="session-status">
|
||||||
💬 Chat Session: <strong>{{ chat_session.get_status_display }}</strong>
|
💬 Chat Session: <strong>{{ chat_session.get_status_display }}</strong>
|
||||||
|
{% if chat_session.status == 'active' %}
|
||||||
|
<span style="color: #10b981; font-size: 12px;">• Active</span>
|
||||||
|
{% elif chat_session.status == 'expired' %}
|
||||||
|
<span style="color: #f59e0b; font-size: 12px;">• Expired</span>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<div class="session-id">ID: {{ chat_session.session_id }}</div>
|
<div class="session-id">ID: {{ chat_session.session_id }}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -348,12 +348,16 @@ def start_chat_session(request):
|
|||||||
# Create new chat session
|
# Create new chat session
|
||||||
session_id = f"{int(time.time() * 1000)}_{uuid.uuid4().hex[:8]}"
|
session_id = f"{int(time.time() * 1000)}_{uuid.uuid4().hex[:8]}"
|
||||||
|
|
||||||
|
from django.utils import timezone
|
||||||
|
from datetime import timedelta
|
||||||
|
|
||||||
chat_session = ChatSession.objects.create(
|
chat_session = ChatSession.objects.create(
|
||||||
session_id=session_id,
|
session_id=session_id,
|
||||||
agent=agent,
|
agent=agent,
|
||||||
user=request.user,
|
user=request.user,
|
||||||
fee_charged=agent.price,
|
fee_charged=agent.price,
|
||||||
status='active'
|
status='active',
|
||||||
|
expires_at=timezone.now() + timedelta(hours=2)
|
||||||
)
|
)
|
||||||
|
|
||||||
# Deduct fee from wallet (if wallet system is implemented)
|
# Deduct fee from wallet (if wallet system is implemented)
|
||||||
@ -392,6 +396,12 @@ def send_chat_message(request):
|
|||||||
status='active'
|
status='active'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Check if session is expired
|
||||||
|
if chat_session.is_expired():
|
||||||
|
chat_session.status = 'expired'
|
||||||
|
chat_session.save()
|
||||||
|
return Response({'error': 'Chat session has expired'}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
# Save user message
|
# Save user message
|
||||||
user_message = ChatMessage.objects.create(
|
user_message = ChatMessage.objects.create(
|
||||||
session=chat_session,
|
session=chat_session,
|
||||||
@ -424,19 +434,47 @@ def send_chat_message(request):
|
|||||||
|
|
||||||
if response.status_code == 200:
|
if response.status_code == 200:
|
||||||
response_data = response.json()
|
response_data = response.json()
|
||||||
agent_response = response_data.get('response', 'I received your message but couldn\'t generate a response.')
|
|
||||||
|
# Try multiple possible response field names from N8N
|
||||||
|
agent_response = None
|
||||||
|
possible_fields = ['output', 'response', 'message', 'reply', 'result', 'text', 'content']
|
||||||
|
|
||||||
|
# Handle array response first (your N8N case)
|
||||||
|
if isinstance(response_data, list) and len(response_data) > 0:
|
||||||
|
first_item = response_data[0]
|
||||||
|
if isinstance(first_item, dict):
|
||||||
|
for field in possible_fields:
|
||||||
|
if field in first_item:
|
||||||
|
agent_response = first_item[field]
|
||||||
|
break
|
||||||
|
elif isinstance(first_item, str):
|
||||||
|
agent_response = first_item
|
||||||
|
|
||||||
|
# Handle direct object response
|
||||||
|
elif isinstance(response_data, dict):
|
||||||
|
for field in possible_fields:
|
||||||
|
if field in response_data:
|
||||||
|
agent_response = response_data[field]
|
||||||
|
break
|
||||||
|
|
||||||
|
# If response_data is a string itself
|
||||||
|
elif isinstance(response_data, str):
|
||||||
|
agent_response = response_data
|
||||||
|
|
||||||
|
# Fallback with full response data for debugging
|
||||||
|
if agent_response is None:
|
||||||
|
agent_response = f"N8N Response received but couldn't parse: {str(response_data)[:200]}..."
|
||||||
|
|
||||||
# Save agent response
|
# Save agent response
|
||||||
agent_message = ChatMessage.objects.create(
|
agent_message = ChatMessage.objects.create(
|
||||||
session=chat_session,
|
session=chat_session,
|
||||||
message_type='agent',
|
message_type='agent',
|
||||||
content=agent_response,
|
content=str(agent_response),
|
||||||
metadata={'webhook_response': response_data}
|
metadata={'webhook_response': response_data, 'raw_response': response.text}
|
||||||
)
|
)
|
||||||
|
|
||||||
# Update session timestamp
|
# Update session timestamp and extend expiration
|
||||||
chat_session.updated_at = timezone.now()
|
chat_session.extend_session()
|
||||||
chat_session.save()
|
|
||||||
|
|
||||||
return Response({
|
return Response({
|
||||||
'user_message': {
|
'user_message': {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user