From d750857e4eebcc65fedbec2f0eeb0d4c011e1534 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 1 Aug 2025 15:45:16 +0530 Subject: [PATCH] =?UTF-8?q?=E2=8F=B0=20Implement=20chat=20session=20timeou?= =?UTF-8?q?t=20system?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- .../commands/cleanup_expired_sessions.py | 30 ++++++++ ...ion_expires_at_alter_chatsession_status.py | 40 +++++++++++ agents/models.py | 24 +++++++ agents/templates/agents/agent_chat.html | 70 +++++++++++++++++++ agents/views.py | 52 ++++++++++++-- 5 files changed, 209 insertions(+), 7 deletions(-) create mode 100644 agents/management/commands/cleanup_expired_sessions.py create mode 100644 agents/migrations/0003_chatsession_expires_at_alter_chatsession_status.py diff --git a/agents/management/commands/cleanup_expired_sessions.py b/agents/management/commands/cleanup_expired_sessions.py new file mode 100644 index 0000000..bdc1b3a --- /dev/null +++ b/agents/management/commands/cleanup_expired_sessions.py @@ -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') \ No newline at end of file diff --git a/agents/migrations/0003_chatsession_expires_at_alter_chatsession_status.py b/agents/migrations/0003_chatsession_expires_at_alter_chatsession_status.py new file mode 100644 index 0000000..e14e53c --- /dev/null +++ b/agents/migrations/0003_chatsession_expires_at_alter_chatsession_status.py @@ -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, + ), + ), + ] diff --git a/agents/models.py b/agents/models.py index 25ae543..97cae91 100644 --- a/agents/models.py +++ b/agents/models.py @@ -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 = [ diff --git a/agents/templates/agents/agent_chat.html b/agents/templates/agents/agent_chat.html index 9a55fe6..50db12c 100644 --- a/agents/templates/agents/agent_chat.html +++ b/agents/templates/agents/agent_chat.html @@ -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 }}');
💬 Chat Session: {{ chat_session.get_status_display }} + {% if chat_session.status == 'active' %} + • Active + {% elif chat_session.status == 'expired' %} + • Expired + {% endif %}
ID: {{ chat_session.session_id }}
diff --git a/agents/views.py b/agents/views.py index b89e977..0335091 100644 --- a/agents/views.py +++ b/agents/views.py @@ -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': {