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:
Claude 2025-08-01 15:45:16 +05:30
parent ab2360a91d
commit d750857e4e
5 changed files with 209 additions and 7 deletions

View 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')

View File

@ -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,
),
),
]

View File

@ -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'),

View File

@ -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>

View File

@ -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': {