mirror of
https://github.com/thecyberlearn/quantum-ai-v2.git
synced 2026-08-18 08:52:59 +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 = [
|
||||
('active', 'Active'),
|
||||
('completed', 'Completed'),
|
||||
('expired', 'Expired'),
|
||||
('abandoned', 'Abandoned'),
|
||||
('failed', 'Failed'),
|
||||
]
|
||||
@ -84,10 +85,19 @@ class ChatSession(models.Model):
|
||||
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='active')
|
||||
context_data = models.JSONField(default=dict, help_text="Session context and progress tracking")
|
||||
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)
|
||||
updated_at = models.DateTimeField(auto_now=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:
|
||||
ordering = ['-created_at']
|
||||
indexes = [
|
||||
@ -97,6 +107,20 @@ class ChatSession(models.Model):
|
||||
|
||||
def __str__(self):
|
||||
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):
|
||||
MESSAGE_TYPE_CHOICES = [
|
||||
|
||||
@ -13,6 +13,71 @@
|
||||
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 {
|
||||
background: var(--surface);
|
||||
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-status">
|
||||
💬 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 class="session-id">ID: {{ chat_session.session_id }}</div>
|
||||
</div>
|
||||
|
||||
@ -348,12 +348,16 @@ def start_chat_session(request):
|
||||
# Create new chat session
|
||||
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(
|
||||
session_id=session_id,
|
||||
agent=agent,
|
||||
user=request.user,
|
||||
fee_charged=agent.price,
|
||||
status='active'
|
||||
status='active',
|
||||
expires_at=timezone.now() + timedelta(hours=2)
|
||||
)
|
||||
|
||||
# Deduct fee from wallet (if wallet system is implemented)
|
||||
@ -392,6 +396,12 @@ def send_chat_message(request):
|
||||
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
|
||||
user_message = ChatMessage.objects.create(
|
||||
session=chat_session,
|
||||
@ -424,19 +434,47 @@ def send_chat_message(request):
|
||||
|
||||
if response.status_code == 200:
|
||||
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
|
||||
agent_message = ChatMessage.objects.create(
|
||||
session=chat_session,
|
||||
message_type='agent',
|
||||
content=agent_response,
|
||||
metadata={'webhook_response': response_data}
|
||||
content=str(agent_response),
|
||||
metadata={'webhook_response': response_data, 'raw_response': response.text}
|
||||
)
|
||||
|
||||
# Update session timestamp
|
||||
chat_session.updated_at = timezone.now()
|
||||
chat_session.save()
|
||||
# Update session timestamp and extend expiration
|
||||
chat_session.extend_session()
|
||||
|
||||
return Response({
|
||||
'user_message': {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user