diff --git a/agents/api_views.py b/agents/api_views.py
new file mode 100644
index 0000000..b3541b6
--- /dev/null
+++ b/agents/api_views.py
@@ -0,0 +1,149 @@
+"""
+REST API views for agent execution and management.
+Handles API endpoints for executing agents, retrieving execution history, etc.
+"""
+
+from rest_framework import status
+from rest_framework.decorators import api_view, permission_classes
+from rest_framework.permissions import IsAuthenticated
+from rest_framework.response import Response
+from rest_framework.pagination import PageNumberPagination
+from django.shortcuts import get_object_or_404
+from django.utils import timezone
+from .models import AgentExecution
+from .serializers import AgentExecutionSerializer
+from .services import AgentFileService
+from .utils import validate_webhook_url, format_agent_message
+import requests
+import time
+import uuid
+
+
+@api_view(['POST'])
+@permission_classes([IsAuthenticated])
+def execute_agent(request):
+ """Execute an agent with provided input data"""
+ agent_slug = request.data.get('agent_slug')
+ input_data = request.data.get('input_data', {})
+
+ if not agent_slug:
+ return Response({'error': 'agent_slug is required'}, status=status.HTTP_400_BAD_REQUEST)
+
+ agent_data = AgentFileService.get_agent_by_slug(agent_slug)
+ if not agent_data or not agent_data.get('is_active', True):
+ return Response({'error': 'Agent not found'}, status=status.HTTP_404_NOT_FOUND)
+
+ agent_price = float(agent_data['price'])
+
+ # Check if user has sufficient balance (using existing wallet system)
+ if hasattr(request.user, 'has_sufficient_balance') and not request.user.has_sufficient_balance(agent_price):
+ return Response({'error': 'Insufficient wallet balance'}, status=status.HTTP_400_BAD_REQUEST)
+
+ # Create execution record
+ execution = AgentExecution.objects.create(
+ agent_slug=agent_data['slug'],
+ agent_name=agent_data['name'],
+ user=request.user,
+ input_data=input_data,
+ fee_charged=agent_price,
+ status='pending'
+ )
+
+ try:
+ # Deduct fee from user wallet (using existing wallet system)
+ if hasattr(request.user, 'deduct_balance'):
+ success = request.user.deduct_balance(
+ agent_price,
+ f'{agent_data["name"]} - Execution {str(execution.id)[:8]}',
+ agent_data['slug']
+ )
+ if not success:
+ execution.status = 'failed'
+ execution.error_message = 'Failed to deduct wallet balance'
+ execution.save()
+ return Response({'error': 'Failed to deduct wallet balance'}, status=status.HTTP_400_BAD_REQUEST)
+
+ # Validate webhook URL to prevent SSRF attacks
+ try:
+ validate_webhook_url(agent_data['webhook_url'])
+ except ValueError as e:
+ execution.status = 'failed'
+ execution.error_message = f'Invalid webhook URL: {str(e)}'
+ execution.save()
+ return Response({'error': f'Invalid webhook URL: {str(e)}'}, status=status.HTTP_400_BAD_REQUEST)
+
+ # Call n8n webhook with proper payload format
+ execution.status = 'running'
+ execution.save()
+
+ # Generate session ID
+ session_id = f"session_{int(time.time() * 1000)}_{str(uuid.uuid4())[:8]}"
+
+ # Format message text for N8N based on agent type
+ message_text = format_agent_message(agent_data['slug'], input_data)
+
+ webhook_payload = {
+ 'sessionId': session_id,
+ 'message': {'text': message_text},
+ 'webhookUrl': agent_data['webhook_url'],
+ 'executionMode': 'production',
+ 'agentId': agent_data['slug'],
+ 'executionId': str(execution.id),
+ 'userId': str(request.user.id)
+ }
+
+ response = requests.post(
+ agent_data['webhook_url'],
+ json=webhook_payload,
+ timeout=90, # Increased timeout for complex processing
+ headers={'Content-Type': 'application/json'}
+ )
+
+ # Store webhook response
+ execution.webhook_response = response.json() if response.headers.get('content-type', '').startswith('application/json') else {'raw': response.text}
+
+ if response.status_code == 200:
+ execution.status = 'completed'
+ execution.output_data = execution.webhook_response
+ else:
+ execution.status = 'failed'
+ execution.error_message = f"Webhook returned {response.status_code}: {response.text[:500]}"
+
+ execution.completed_at = timezone.now()
+ execution.save()
+
+ serializer = AgentExecutionSerializer(execution)
+ return Response(serializer.data, status=status.HTTP_201_CREATED)
+
+ except requests.RequestException as e:
+ execution.status = 'failed'
+ execution.error_message = str(e)
+ execution.completed_at = timezone.now()
+ execution.save()
+
+ return Response({
+ 'error': 'Failed to execute agent',
+ 'execution_id': str(execution.id)
+ }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
+
+
+@api_view(['GET'])
+@permission_classes([IsAuthenticated])
+def execution_list(request):
+ """List user's agent executions"""
+ executions = AgentExecution.objects.filter(user=request.user)
+
+ paginator = PageNumberPagination()
+ paginator.page_size = 20
+ result_page = paginator.paginate_queryset(executions, request)
+ serializer = AgentExecutionSerializer(result_page, many=True)
+ return paginator.get_paginated_response(serializer.data)
+
+
+@api_view(['GET'])
+@permission_classes([IsAuthenticated])
+def execution_detail(request, execution_id):
+ """Get detailed execution information"""
+ execution = get_object_or_404(AgentExecution, id=execution_id, user=request.user)
+ serializer = AgentExecutionSerializer(execution)
+ return Response(serializer.data)
\ No newline at end of file
diff --git a/agents/chat_views.py b/agents/chat_views.py
new file mode 100644
index 0000000..d055336
--- /dev/null
+++ b/agents/chat_views.py
@@ -0,0 +1,552 @@
+"""
+Chat functionality views for chat-based agents.
+Handles chat sessions, message sending, session management, and chat exports.
+"""
+
+from rest_framework import status
+from rest_framework.decorators import api_view, permission_classes
+from rest_framework.permissions import IsAuthenticated
+from rest_framework.response import Response
+from django.shortcuts import get_object_or_404, render
+from django.utils import timezone
+from django.contrib.auth.decorators import login_required
+from django.http import HttpResponse
+from reportlab.pdfgen import canvas
+from reportlab.lib.pagesizes import letter
+from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
+from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer
+from reportlab.lib.units import inch
+from io import BytesIO
+from .models import ChatSession, ChatMessage
+from .services import AgentFileService
+from .utils import validate_webhook_url, AgentCompat
+import requests
+import time
+import uuid
+
+
+@api_view(['POST'])
+@permission_classes([IsAuthenticated])
+def start_chat_session(request):
+ """Start a new chat session"""
+ agent_slug = request.data.get('agent_slug')
+
+ if not agent_slug:
+ return Response({'error': 'agent_slug is required'}, status=status.HTTP_400_BAD_REQUEST)
+
+ agent_data = AgentFileService.get_agent_by_slug(agent_slug)
+ if not agent_data or not agent_data.get('is_active', True) or agent_data.get('agent_type') != 'chat':
+ return Response({'error': 'Chat agent not found'}, status=status.HTTP_404_NOT_FOUND)
+
+ agent_price = float(agent_data['price'])
+
+ # Check wallet balance
+ if hasattr(request.user, 'wallet_balance') and request.user.wallet_balance < agent_price:
+ return Response({'error': 'Insufficient wallet balance'}, status=status.HTTP_400_BAD_REQUEST)
+
+ # Check for existing active session (using slug-based lookup)
+ existing_session = ChatSession.objects.filter(
+ agent_slug=agent_data['slug'],
+ user=request.user,
+ status='active'
+ ).first()
+
+ if existing_session:
+ return Response({
+ 'session_id': existing_session.session_id,
+ 'message': 'Active session already exists'
+ })
+
+ # Create new chat session
+ session_id = f"{int(time.time() * 1000)}_{uuid.uuid4().hex[:8]}"
+
+ from datetime import timedelta
+
+ chat_session = ChatSession.objects.create(
+ session_id=session_id,
+ agent_slug=agent_data['slug'],
+ agent_name=agent_data['name'],
+ user=request.user,
+ fee_charged=agent_price,
+ status='active',
+ expires_at=timezone.now() + timedelta(minutes=30)
+ )
+
+ # Deduct fee from wallet
+ try:
+ success = request.user.deduct_balance(
+ agent_price,
+ f'{agent_data["name"]} - Chat Session {session_id}',
+ agent_data['slug']
+ )
+ if not success:
+ # Delete the created session if payment fails
+ chat_session.delete()
+ return Response({
+ 'error': 'Failed to process payment. Please check your wallet balance.'
+ }, status=status.HTTP_400_BAD_REQUEST)
+ except Exception as e:
+ # Delete the created session if payment processing fails
+ chat_session.delete()
+ return Response({
+ 'error': 'Payment processing error. Please try again.'
+ }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
+
+ # Send welcome message
+ welcome_message = f"""## Welcome to {agent_data["name"]}! 🔍
+
+I'm here to guide you through the **5 Whys methodology** - a powerful problem-solving technique to uncover root causes.
+
+### How It Works:
+• **Ask "Why" 5 times** to drill down from symptoms to root causes
+• **Systematic analysis** of Occurrence, Detection, and Prevention
+• **Actionable insights** for effective solutions
+
+### Getting Started:
+Please describe the **specific problem** you'd like to analyze. Include:
+- What happened?
+- When did it occur?
+- What are the immediate impacts?
+
+Let's discover the root cause together! 💪"""
+
+ ChatMessage.objects.create(
+ session=chat_session,
+ message_type='agent',
+ content=welcome_message
+ )
+
+ return Response({
+ 'session_id': chat_session.session_id,
+ 'message': 'Chat session started successfully'
+ })
+
+
+@api_view(['POST'])
+@permission_classes([IsAuthenticated])
+def send_chat_message(request):
+ """Send a message in a chat session"""
+ session_id = request.data.get('session_id')
+ message_content = request.data.get('message', '').strip()
+
+ if not session_id or not message_content:
+ return Response({'error': 'session_id and message are required'}, status=status.HTTP_400_BAD_REQUEST)
+
+ # Get chat session
+ chat_session = get_object_or_404(
+ ChatSession,
+ session_id=session_id,
+ user=request.user,
+ 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)
+
+ # Get agent data for message limit
+ agent_data = AgentFileService.get_agent_by_slug(chat_session.agent_slug)
+ message_limit = agent_data.get('message_limit', 50) if agent_data else 50
+
+ # Check message limit (only count user messages)
+ current_user_message_count = ChatMessage.objects.filter(session=chat_session, message_type='user').count()
+ if current_user_message_count >= message_limit:
+ # Auto-complete the session when message limit is reached
+ chat_session.status = 'completed'
+ chat_session.completed_at = timezone.now()
+ chat_session.save()
+
+ return Response({
+ 'error': f'Message limit reached ({message_limit} messages). Session completed. You can download your conversation or start a new session.'
+ }, status=status.HTTP_400_BAD_REQUEST)
+
+ # Save user message
+ user_message = ChatMessage.objects.create(
+ session=chat_session,
+ message_type='user',
+ content=message_content
+ )
+
+ # Prepare webhook payload
+ webhook_payload = {
+ "message": {
+ "text": f"""User message: "{message_content}"
+
+Provide helpful 5 Whys analysis guidance with professional formatting:
+
+FORMATTING REQUIREMENTS:
+- Use markdown headers (##, ###) for sections
+- Use **bold** for key terms and emphasis
+- Use bullet points (•) for lists
+- Use numbered lists (1., 2., 3.) for steps
+- Structure responses with clear sections
+- Add relevant emojis for engagement
+
+CONTENT GUIDELINES:
+- Guide through 5 Whys methodology systematically
+- Ask probing questions about Occurrence, Detection, Prevention
+- Help user drill down from symptoms to root causes
+- Keep responses conversational but structured
+- Do not generate final reports - focus on interactive guidance
+- Encourage deeper thinking with follow-up questions"""
+ },
+ "sessionId": session_id,
+ "userId": str(request.user.id),
+ "agentId": chat_session.agent_slug,
+ "messageType": "chat"
+ }
+
+ try:
+ # Get webhook URL from agent data
+ webhook_url = agent_data['webhook_url'] if agent_data else None
+ if not webhook_url:
+ raise ValueError("Agent webhook URL not found")
+
+ # Validate webhook URL
+ validate_webhook_url(webhook_url)
+
+ # Send to webhook
+ response = requests.post(
+ webhook_url,
+ json=webhook_payload,
+ timeout=30,
+ headers={'Content-Type': 'application/json'}
+ )
+
+ if response.status_code == 200:
+ response_data = response.json()
+
+ # 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=str(agent_response),
+ metadata={'webhook_response': response_data, 'raw_response': response.text}
+ )
+
+ # Update session timestamp and extend expiration
+ chat_session.extend_session()
+
+ return Response({
+ 'user_message': {
+ 'id': str(user_message.id),
+ 'content': user_message.content,
+ 'timestamp': user_message.timestamp.isoformat()
+ },
+ 'agent_message': {
+ 'id': str(agent_message.id),
+ 'content': agent_message.content,
+ 'timestamp': agent_message.timestamp.isoformat()
+ }
+ })
+ else:
+ # Webhook error
+ error_message = "I'm having trouble processing your message right now. Please try again."
+ agent_message = ChatMessage.objects.create(
+ session=chat_session,
+ message_type='agent',
+ content=error_message,
+ metadata={'error': f'Webhook returned {response.status_code}'}
+ )
+
+ return Response({
+ 'user_message': {
+ 'id': str(user_message.id),
+ 'content': user_message.content,
+ 'timestamp': user_message.timestamp.isoformat()
+ },
+ 'agent_message': {
+ 'id': str(agent_message.id),
+ 'content': agent_message.content,
+ 'timestamp': agent_message.timestamp.isoformat()
+ }
+ }, status=status.HTTP_202_ACCEPTED)
+
+ except Exception as e:
+ # Handle webhook errors
+ error_message = "I'm experiencing technical difficulties. Please try again later."
+ agent_message = ChatMessage.objects.create(
+ session=chat_session,
+ message_type='agent',
+ content=error_message,
+ metadata={'error': str(e)}
+ )
+
+ return Response({
+ 'user_message': {
+ 'id': str(user_message.id),
+ 'content': user_message.content,
+ 'timestamp': user_message.timestamp.isoformat()
+ },
+ 'agent_message': {
+ 'id': str(agent_message.id),
+ 'content': agent_message.content,
+ 'timestamp': agent_message.timestamp.isoformat()
+ }
+ }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
+
+
+@api_view(['GET'])
+@permission_classes([IsAuthenticated])
+def get_chat_history(request, session_id):
+ """Get chat history for a session"""
+ chat_session = get_object_or_404(
+ ChatSession,
+ session_id=session_id,
+ user=request.user
+ )
+
+ messages = ChatMessage.objects.filter(session=chat_session).order_by('timestamp')
+
+ message_data = []
+ for message in messages:
+ message_data.append({
+ 'id': str(message.id),
+ 'message_type': message.message_type,
+ 'content': message.content,
+ 'timestamp': message.timestamp.isoformat()
+ })
+
+ return Response({
+ 'session_id': session_id,
+ 'status': chat_session.status,
+ 'messages': message_data
+ })
+
+
+@api_view(['POST'])
+@permission_classes([IsAuthenticated])
+def end_chat_session(request):
+ """End a chat session"""
+ session_id = request.data.get('session_id')
+
+ if not session_id:
+ return Response({'error': 'session_id is required'}, status=status.HTTP_400_BAD_REQUEST)
+
+ chat_session = get_object_or_404(
+ ChatSession,
+ session_id=session_id,
+ user=request.user,
+ status='active'
+ )
+
+ chat_session.status = 'completed'
+ chat_session.completed_at = timezone.now()
+ chat_session.save()
+
+ return Response({'message': 'Chat session ended successfully'})
+
+
+@api_view(['GET'])
+@permission_classes([IsAuthenticated])
+def get_session_status(request, session_id):
+ """Get real-time session status data"""
+ chat_session = get_object_or_404(
+ ChatSession,
+ session_id=session_id,
+ user=request.user
+ )
+
+ # Time calculations
+ now = timezone.now()
+ time_remaining_seconds = max(0, (chat_session.expires_at - now).total_seconds())
+ time_remaining_minutes = int(time_remaining_seconds // 60)
+ time_remaining_hours = time_remaining_minutes // 60
+ time_remaining_minutes = time_remaining_minutes % 60
+
+ if time_remaining_hours > 0:
+ time_remaining_str = f"{time_remaining_hours}h {time_remaining_minutes}m"
+ else:
+ time_remaining_str = f"{time_remaining_minutes}m"
+
+ # Time percentage (how much time is left)
+ total_session_time = 30 * 60 # 30 minutes in seconds
+ time_percentage = max(0, min(100, (time_remaining_seconds / total_session_time) * 100))
+
+ # Get agent data for message limit
+ agent_data = AgentFileService.get_agent_by_slug(chat_session.agent_slug)
+ message_limit = agent_data.get('message_limit', 50) if agent_data else 50
+
+ # Message calculations (only count user messages)
+ message_count = ChatMessage.objects.filter(session=chat_session, message_type='user').count()
+ message_percentage = min(100, (message_count / message_limit) * 100)
+
+ return Response({
+ 'success': True,
+ 'session_id': session_id,
+ 'status': chat_session.status,
+ 'time_remaining_seconds': int(time_remaining_seconds),
+ 'time_remaining_str': time_remaining_str,
+ 'time_percentage': int(time_percentage),
+ 'message_count': message_count,
+ 'message_limit': message_limit,
+ 'message_percentage': int(message_percentage),
+ 'is_expired': chat_session.is_expired()
+ })
+
+
+@login_required
+def export_chat(request, session_id):
+ """Export chat session as PDF or TXT"""
+ format_type = request.GET.get('format', 'pdf').lower()
+
+ # Get chat session and verify ownership
+ chat_session = get_object_or_404(
+ ChatSession,
+ session_id=session_id,
+ user=request.user
+ )
+
+ # Get all messages for this session
+ messages = ChatMessage.objects.filter(session=chat_session).order_by('timestamp')
+
+ if not messages.exists():
+ return HttpResponse('No messages found in this chat session.', status=404)
+
+ if format_type == 'pdf':
+ return export_chat_pdf(chat_session, messages)
+ elif format_type == 'txt':
+ return export_chat_txt(chat_session, messages)
+ else:
+ return HttpResponse('Invalid format. Use pdf or txt.', status=400)
+
+
+def export_chat_pdf(chat_session, messages):
+ """Generate PDF export of chat session"""
+ buffer = BytesIO()
+ doc = SimpleDocTemplate(buffer, pagesize=letter)
+ styles = getSampleStyleSheet()
+ story = []
+
+ # Title
+ title_style = ParagraphStyle(
+ 'CustomTitle',
+ parent=styles['Heading1'],
+ fontSize=18,
+ spaceAfter=30,
+ alignment=1 # Center alignment
+ )
+
+ story.append(Paragraph(f"5 Whys Analysis - {chat_session.agent_name}", title_style))
+ story.append(Spacer(1, 12))
+
+ # Session info
+ info_style = styles['Normal']
+ story.append(Paragraph(f"Session ID: {chat_session.session_id}", info_style))
+ story.append(Paragraph(f"Date: {chat_session.created_at.strftime('%B %d, %Y at %I:%M %p')}", info_style))
+ story.append(Paragraph(f"Agent: {chat_session.agent_name}", info_style))
+ story.append(Paragraph(f"Total Messages: {messages.count()}", info_style))
+ story.append(Spacer(1, 20))
+
+ # Messages
+ user_style = ParagraphStyle(
+ 'UserMessage',
+ parent=styles['Normal'],
+ leftIndent=0,
+ rightIndent=50,
+ spaceBefore=12,
+ spaceAfter=6,
+ fontSize=10
+ )
+
+ agent_style = ParagraphStyle(
+ 'AgentMessage',
+ parent=styles['Normal'],
+ leftIndent=50,
+ rightIndent=0,
+ spaceBefore=12,
+ spaceAfter=6,
+ fontSize=10
+ )
+
+ for message in messages:
+ timestamp = message.timestamp.strftime('%I:%M %p')
+
+ if message.message_type == 'user':
+ story.append(Paragraph(f"You ({timestamp}):
{message.content}", user_style))
+ elif message.message_type == 'agent':
+ story.append(Paragraph(f"{chat_session.agent_name} ({timestamp}):
{message.content}", agent_style))
+ elif message.message_type == 'system':
+ story.append(Paragraph(f"System ({timestamp}): {message.content}", styles['Normal']))
+
+ # Build PDF
+ doc.build(story)
+ buffer.seek(0)
+
+ response = HttpResponse(buffer.getvalue(), content_type='application/pdf')
+ response['Content-Disposition'] = f'attachment; filename="5whys_chat_{chat_session.session_id}.pdf"'
+ return response
+
+
+def export_chat_txt(chat_session, messages):
+ """Generate TXT export of chat session"""
+ content = []
+ content.append("=" * 60)
+ content.append(f"5 Whys Analysis - {chat_session.agent_name}")
+ content.append("=" * 60)
+ content.append("")
+ content.append(f"Session ID: {chat_session.session_id}")
+ content.append(f"Date: {chat_session.created_at.strftime('%B %d, %Y at %I:%M %p')}")
+ content.append(f"Agent: {chat_session.agent_name}")
+ content.append(f"Total Messages: {messages.count()}")
+ content.append("")
+ content.append("-" * 60)
+ content.append("CONVERSATION")
+ content.append("-" * 60)
+ content.append("")
+
+ for message in messages:
+ timestamp = message.timestamp.strftime('%I:%M %p')
+
+ if message.message_type == 'user':
+ content.append(f"You ({timestamp}):")
+ content.append(message.content)
+ elif message.message_type == 'agent':
+ content.append(f"{chat_session.agent_name} ({timestamp}):")
+ content.append(message.content)
+ elif message.message_type == 'system':
+ content.append(f"System ({timestamp}): {message.content}")
+
+ content.append("") # Empty line between messages
+
+ content.append("-" * 60)
+ content.append("End of Conversation")
+ content.append("-" * 60)
+
+ text_content = "\n".join(content)
+
+ response = HttpResponse(text_content, content_type='text/plain')
+ response['Content-Disposition'] = f'attachment; filename="5whys_chat_{chat_session.session_id}.txt"'
+ return response
\ No newline at end of file
diff --git a/agents/direct_access_views.py b/agents/direct_access_views.py
new file mode 100644
index 0000000..b4fff48
--- /dev/null
+++ b/agents/direct_access_views.py
@@ -0,0 +1,470 @@
+"""
+Direct access views for external form agents.
+Handles payment processing and access to external forms (JotForm, Google Forms, etc.).
+"""
+
+from django.shortcuts import render, redirect
+from django.contrib.auth.decorators import login_required
+from django.contrib import messages
+from django.utils import timezone
+from django.http import Http404
+from datetime import timedelta
+from .models import AgentExecution
+from .services import AgentFileService
+from .utils import AgentCompat
+
+
+def career_navigator_access(request):
+ """Handle Try Now button click - charge wallet and redirect to form"""
+ if not request.user.is_authenticated:
+ # Clear all existing messages before adding login message
+ storage = messages.get_messages(request)
+ for _ in storage:
+ pass # Consume all messages
+ # Add login message to session for after login redirect
+ request.session['post_login_message'] = 'Please complete your login to access the Career Navigator.'
+ return redirect('authentication:login')
+
+ # Get the career navigator agent
+ agent_data = AgentFileService.get_agent_by_slug('cybersec-career-navigator')
+ if not agent_data or not agent_data.get('is_active', True):
+ messages.error(request, 'Career Navigator is currently unavailable.')
+ return redirect('agents:marketplace')
+
+ agent_price = float(agent_data['price'])
+
+ # Check if user has sufficient balance
+ if not request.user.has_sufficient_balance(agent_price):
+ messages.error(request, f'Insufficient balance! You need {agent_price} AED to access the Career Navigator.')
+ return redirect('wallet:wallet')
+
+ # Deduct fee from user wallet
+ success = request.user.deduct_balance(
+ agent_price,
+ f'{agent_data["name"]} - Direct Access',
+ agent_data['slug']
+ )
+
+ if not success:
+ messages.error(request, 'Failed to process payment. Please try again.')
+ return redirect('agents:marketplace')
+
+ # Create execution record for tracking
+ execution = AgentExecution.objects.create(
+ agent_slug=agent_data['slug'],
+ agent_name=agent_data['name'],
+ user=request.user,
+ input_data={'action': 'direct_access', 'source': 'try_now_button'},
+ fee_charged=agent_price,
+ status='completed',
+ output_data={
+ 'type': 'direct_access',
+ 'message': f'Direct access granted to {agent_data["name"]}',
+ 'access_method': 'try_now_button'
+ },
+ completed_at=timezone.now()
+ )
+
+ # Redirect directly to form - no message needed
+ return redirect('agents:career_navigator')
+
+
+def career_navigator_view(request):
+ """Display the career navigator form page"""
+ if not request.user.is_authenticated:
+ # Clear all existing messages before adding login message
+ storage = messages.get_messages(request)
+ for _ in storage:
+ pass # Consume all messages
+ # Add login message to session for after login redirect
+ request.session['post_login_message'] = 'Please complete your login to access the Career Navigator.'
+ return redirect('authentication:login')
+
+ # Get the career navigator agent
+ agent_data = AgentFileService.get_agent_by_slug('cybersec-career-navigator')
+ if not agent_data or not agent_data.get('is_active', True):
+ messages.error(request, 'Career Navigator is currently unavailable.')
+ return redirect('agents:marketplace')
+
+ # Convert to compatible object
+ agent = AgentCompat(agent_data)
+
+ # Check if user has a recent execution (within last 2 hours) or just redirect to payment
+ recent_execution = AgentExecution.objects.filter(
+ agent_slug=agent.slug, # Changed to slug-based lookup
+ user=request.user,
+ status='completed',
+ created_at__gte=timezone.now() - timedelta(hours=2)
+ ).first()
+
+ if not recent_execution:
+ messages.info(request, 'Please click "Try Now" to access your Career Navigator consultation.')
+ return redirect('agents:marketplace')
+
+ context = {
+ 'agent': agent,
+ 'form_url': agent.webhook_url,
+ 'user_balance': request.user.wallet_balance,
+ 'execution': recent_execution
+ }
+
+ return render(request, 'career_navigator.html', context)
+
+
+def ai_brand_strategist_view(request):
+ """Display the AI Brand Strategist form page"""
+ if not request.user.is_authenticated:
+ # Clear all existing messages before adding login message
+ storage = messages.get_messages(request)
+ for _ in storage:
+ pass # Consume all messages
+ # Add login message to session for after login redirect
+ request.session['post_login_message'] = 'Please complete your login to access the AI Brand Strategist.'
+ return redirect('authentication:login')
+
+ # Get the AI Brand Strategist agent
+ agent_data = AgentFileService.get_agent_by_slug('ai-brand-strategist')
+ if not agent_data or not agent_data.get('is_active', True):
+ messages.error(request, 'AI Brand Strategist is currently unavailable.')
+ return redirect('agents:marketplace')
+
+ # Convert to compatible object
+ agent = AgentCompat(agent_data)
+
+ # Check if user has a recent execution (within last 2 hours) or just redirect to payment
+ recent_execution = AgentExecution.objects.filter(
+ agent_slug=agent.slug, # Changed to slug-based lookup
+ user=request.user,
+ status='completed',
+ created_at__gte=timezone.now() - timedelta(hours=2)
+ ).first()
+
+ if not recent_execution:
+ messages.info(request, 'Please click "Try Now" to access your AI Brand Strategist consultation.')
+ return redirect('agents:marketplace')
+
+ context = {
+ 'agent': agent,
+ 'form_url': agent.webhook_url,
+ 'user_balance': request.user.wallet_balance,
+ 'execution': recent_execution
+ }
+
+ return render(request, 'ai_brand_strategist.html', context)
+
+
+def ai_brand_strategist_access(request):
+ """Handle Try Now button click - charge wallet and redirect to form"""
+ if not request.user.is_authenticated:
+ # Clear all existing messages before adding login message
+ storage = messages.get_messages(request)
+ for _ in storage:
+ pass # Consume all messages
+ # Add login message to session for after login redirect
+ request.session['post_login_message'] = 'Please complete your login to access the AI Brand Strategist.'
+ return redirect('authentication:login')
+
+ # Get the AI Brand Strategist agent
+ agent_data = AgentFileService.get_agent_by_slug('ai-brand-strategist')
+ if not agent_data or not agent_data.get('is_active', True):
+ messages.error(request, 'AI Brand Strategist is currently unavailable.')
+ return redirect('agents:marketplace')
+
+ agent_price = float(agent_data['price'])
+
+ # Check if user has sufficient balance
+ if not request.user.has_sufficient_balance(agent_price):
+ messages.error(request, f'Insufficient balance! You need {agent_price} AED to access the AI Brand Strategist.')
+ return redirect('wallet:wallet')
+
+ # Deduct fee from user wallet
+ success = request.user.deduct_balance(
+ agent_price,
+ f'{agent_data["name"]} - Direct Access',
+ agent_data['slug']
+ )
+
+ if not success:
+ messages.error(request, 'Failed to process payment. Please try again.')
+ return redirect('agents:marketplace')
+
+ # Create execution record for tracking
+ execution = AgentExecution.objects.create(
+ agent_slug=agent_data['slug'],
+ agent_name=agent_data['name'],
+ user=request.user,
+ input_data={'action': 'direct_access', 'source': 'try_now_button'},
+ fee_charged=agent_price,
+ status='completed',
+ output_data={
+ 'type': 'direct_access',
+ 'message': f'Direct access granted to {agent_data["name"]}',
+ 'access_method': 'try_now_button'
+ },
+ completed_at=timezone.now()
+ )
+
+ # Redirect directly to form - no message needed
+ return redirect('agents:ai_brand_strategist')
+
+
+def lean_six_sigma_expert_view(request):
+ """Display the Lean Six Sigma Expert form page"""
+ if not request.user.is_authenticated:
+ # Clear all existing messages before adding login message
+ storage = messages.get_messages(request)
+ for _ in storage:
+ pass # Consume all messages
+ # Add login message to session for after login redirect
+ request.session['post_login_message'] = 'Please complete your login to access the Lean Six Sigma Expert.'
+ return redirect('authentication:login')
+
+ # Get the Lean Six Sigma Expert agent
+ agent_data = AgentFileService.get_agent_by_slug('lean-six-sigma-expert')
+ if not agent_data or not agent_data.get('is_active', True):
+ messages.error(request, 'Lean Six Sigma Expert is currently unavailable.')
+ return redirect('agents:marketplace')
+
+ # Convert to compatible object
+ agent = AgentCompat(agent_data)
+
+ # Check if user has a recent execution (within last 2 hours) or just redirect to payment
+ recent_execution = AgentExecution.objects.filter(
+ agent_slug=agent.slug, # Changed to slug-based lookup
+ user=request.user,
+ status='completed',
+ created_at__gte=timezone.now() - timedelta(hours=2)
+ ).first()
+
+ if not recent_execution:
+ messages.info(request, 'Please click "Try Now" to access your Lean Six Sigma Expert consultation.')
+ return redirect('agents:marketplace')
+
+ context = {
+ 'agent': agent,
+ 'form_url': agent.webhook_url,
+ 'user_balance': request.user.wallet_balance,
+ 'execution': recent_execution
+ }
+
+ return render(request, 'lean_six_sigma_expert.html', context)
+
+
+def lean_six_sigma_expert_access(request):
+ """Handle Try Now button click - charge wallet and redirect to form"""
+ if not request.user.is_authenticated:
+ # Clear all existing messages before adding login message
+ storage = messages.get_messages(request)
+ for _ in storage:
+ pass # Consume all messages
+ # Add login message to session for after login redirect
+ request.session['post_login_message'] = 'Please complete your login to access the Lean Six Sigma Expert.'
+ return redirect('authentication:login')
+
+ # Get the Lean Six Sigma Expert agent
+ agent_data = AgentFileService.get_agent_by_slug('lean-six-sigma-expert')
+ if not agent_data or not agent_data.get('is_active', True):
+ messages.error(request, 'Lean Six Sigma Expert is currently unavailable.')
+ return redirect('agents:marketplace')
+
+ agent_price = float(agent_data['price'])
+
+ # Check if user has sufficient balance
+ if not request.user.has_sufficient_balance(agent_price):
+ messages.error(request, f'Insufficient balance! You need {agent_price} AED to access the Lean Six Sigma Expert.')
+ return redirect('wallet:wallet')
+
+ # Deduct fee from user wallet
+ success = request.user.deduct_balance(
+ agent_price,
+ f'{agent_data["name"]} - Direct Access',
+ agent_data['slug']
+ )
+
+ if not success:
+ messages.error(request, 'Failed to process payment. Please try again.')
+ return redirect('agents:marketplace')
+
+ # Create execution record for tracking
+ execution = AgentExecution.objects.create(
+ agent_slug=agent_data['slug'],
+ agent_name=agent_data['name'],
+ user=request.user,
+ input_data={'action': 'direct_access', 'source': 'try_now_button'},
+ fee_charged=agent_price,
+ status='completed',
+ output_data={
+ 'type': 'direct_access',
+ 'message': f'Direct access granted to {agent_data["name"]}',
+ 'access_method': 'try_now_button'
+ },
+ completed_at=timezone.now()
+ )
+
+ # Redirect directly to form - no message needed
+ return redirect('agents:lean_six_sigma_expert')
+
+
+def swot_analysis_expert_view(request):
+ """Display the SWOT Analysis Expert form page"""
+ if not request.user.is_authenticated:
+ # Clear all existing messages before adding login message
+ storage = messages.get_messages(request)
+ for _ in storage:
+ pass # Consume all messages
+ # Add login message to session for after login redirect
+ request.session['post_login_message'] = 'Please complete your login to access the SWOT Analysis Expert.'
+ return redirect('authentication:login')
+
+ # Get the SWOT Analysis Expert agent
+ agent_data = AgentFileService.get_agent_by_slug('swot-analysis-expert')
+ if not agent_data or not agent_data.get('is_active', True):
+ messages.error(request, 'SWOT Analysis Expert is currently unavailable.')
+ return redirect('agents:marketplace')
+
+ # Convert to compatible object
+ agent = AgentCompat(agent_data)
+
+ # Check if user has a recent execution (within last 2 hours) or just redirect to payment
+ recent_execution = AgentExecution.objects.filter(
+ agent_slug=agent.slug, # Changed to slug-based lookup
+ user=request.user,
+ status='completed',
+ created_at__gte=timezone.now() - timedelta(hours=2)
+ ).first()
+
+ if not recent_execution:
+ messages.info(request, 'Please click "Try Now" to access your SWOT Analysis Expert consultation.')
+ return redirect('agents:marketplace')
+
+ context = {
+ 'agent': agent,
+ 'form_url': agent.webhook_url,
+ 'user_balance': request.user.wallet_balance,
+ 'execution': recent_execution
+ }
+
+ return render(request, 'swot_analysis_expert.html', context)
+
+
+def swot_analysis_expert_access(request):
+ """Handle Try Now button click - charge wallet and redirect to form"""
+ if not request.user.is_authenticated:
+ # Clear all existing messages before adding login message
+ storage = messages.get_messages(request)
+ for _ in storage:
+ pass # Consume all messages
+ # Add login message to session for after login redirect
+ request.session['post_login_message'] = 'Please complete your login to access the SWOT Analysis Expert.'
+ return redirect('authentication:login')
+
+ # Get the SWOT Analysis Expert agent
+ agent_data = AgentFileService.get_agent_by_slug('swot-analysis-expert')
+ if not agent_data or not agent_data.get('is_active', True):
+ messages.error(request, 'SWOT Analysis Expert is currently unavailable.')
+ return redirect('agents:marketplace')
+
+ agent_price = float(agent_data['price'])
+
+ # Check if user has sufficient balance
+ if not request.user.has_sufficient_balance(agent_price):
+ messages.error(request, f'Insufficient balance! You need {agent_price} AED to access the SWOT Analysis Expert.')
+ return redirect('wallet:wallet')
+
+ # Deduct fee from user wallet
+ success = request.user.deduct_balance(
+ agent_price,
+ f'{agent_data["name"]} - Direct Access',
+ agent_data['slug']
+ )
+
+ if not success:
+ messages.error(request, 'Failed to process payment. Please try again.')
+ return redirect('agents:marketplace')
+
+ # Create execution record for tracking
+ execution = AgentExecution.objects.create(
+ agent_slug=agent_data['slug'],
+ agent_name=agent_data['name'],
+ user=request.user,
+ input_data={'action': 'direct_access', 'source': 'try_now_button'},
+ fee_charged=agent_price,
+ status='completed',
+ output_data={
+ 'type': 'direct_access',
+ 'message': f'Direct access granted to {agent_data["name"]}',
+ 'access_method': 'try_now_button'
+ },
+ completed_at=timezone.now()
+ )
+
+ # Redirect directly to form - no message needed
+ return redirect('agents:swot_analysis_expert')
+
+
+@login_required
+def direct_access_handler(request, slug):
+ """
+ Generic handler for direct access agents (external forms like JotForm).
+ Handles payment processing and grants access to external form.
+ """
+ agent_data = AgentFileService.get_agent_by_slug(slug)
+ if not agent_data or not agent_data.get('is_active', True):
+ raise Http404("Agent not found")
+
+ # Convert to compatible object
+ agent = AgentCompat(agent_data)
+
+ # Verify this is a direct access agent
+ if not agent.access_url_name or not agent.display_url_name:
+ messages.error(request, 'This agent does not support direct access.')
+ return redirect('agents:marketplace')
+
+ agent_price = agent.price
+
+ # Handle payment for paid agents
+ if agent_price > 0:
+ user_balance = request.user.wallet_balance
+ if user_balance < agent_price:
+ messages.error(request, f'Insufficient balance. You need {agent_price} AED but have {user_balance} AED.')
+ return redirect('wallet:wallet')
+
+ # Process payment
+ try:
+ from wallet.models import WalletTransaction
+ WalletTransaction.objects.create(
+ user=request.user,
+ amount=-agent_price,
+ type='agent_usage',
+ description=f'Payment for {agent.name}',
+ agent_slug=agent.slug
+ )
+ except Exception as e:
+ messages.error(request, 'Payment processing failed. Please try again.')
+ return redirect('agents:agent_detail', slug=slug)
+
+ # Grant access - redirect directly to display page
+ return redirect('agents:direct_access_display', slug=slug)
+
+
+@login_required
+def direct_access_display(request, slug):
+ """
+ Generic display handler for direct access agents.
+ Shows external form (JotForm, Google Forms, etc.) in iframe or redirects directly.
+ """
+ agent_data = AgentFileService.get_agent_by_slug(slug)
+ if not agent_data or not agent_data.get('is_active', True):
+ raise Http404("Agent not found")
+
+ # Convert to compatible object
+ agent = AgentCompat(agent_data)
+
+ # Verify this is a direct access agent
+ if not agent.access_url_name or not agent.display_url_name:
+ messages.error(request, 'This agent does not support direct access.')
+ return redirect('agents:marketplace')
+
+ # For now, redirect directly to external form
+ # Future: Can render iframe template or custom display logic
+ return redirect(agent.webhook_url)
\ No newline at end of file
diff --git a/agents/utils.py b/agents/utils.py
new file mode 100644
index 0000000..9b50117
--- /dev/null
+++ b/agents/utils.py
@@ -0,0 +1,88 @@
+"""
+Utility functions for the agents app.
+Contains webhook validation, message formatting, and other helper functions.
+"""
+
+import ipaddress
+from urllib.parse import urlparse
+
+
+def validate_webhook_url(url):
+ """
+ Validate webhook URL to prevent SSRF attacks.
+ Only allows HTTPS URLs to external, non-private networks.
+ """
+ try:
+ parsed = urlparse(url)
+
+ # Only allow HTTP/HTTPS protocols
+ if parsed.scheme not in ['http', 'https']:
+ raise ValueError("Only HTTP/HTTPS URLs are allowed")
+
+ # Get hostname
+ hostname = parsed.hostname
+ if not hostname:
+ raise ValueError("Invalid hostname in URL")
+
+ # For localhost development, allow localhost URLs first
+ if hostname in ['localhost', '127.0.0.1'] and parsed.port in [5678, 8000, 8080]:
+ return True # Allow N8N development server
+
+ # Check if hostname is an IP address
+ try:
+ ip = ipaddress.ip_address(hostname)
+ # Block private, loopback, and reserved IP ranges
+ if (ip.is_private or ip.is_loopback or ip.is_reserved or
+ ip.is_link_local or ip.is_multicast):
+ raise ValueError("Internal/private IP addresses are not allowed")
+ except ValueError as e:
+ if "does not appear to be an IPv4 or IPv6 address" not in str(e):
+ raise # Re-raise if it's not just a "not an IP" error
+ # If it's not an IP, it's a domain name - that's fine
+
+ return True
+
+ except Exception as e:
+ raise ValueError(f"Invalid webhook URL: {str(e)}")
+
+
+def format_agent_message(agent_slug, input_data):
+ """Format input data into a message for N8N webhook based on agent type"""
+ if agent_slug == 'social-ads-generator':
+ description = input_data.get('description', '')
+ platform = input_data.get('social_platform', '')
+ emoji = input_data.get('include_emoji', 'yes')
+ language = input_data.get('language', 'English')
+
+ return f"Execute Social Media Ad Creator with the following parameters:. Describe what you'd like to generate: {description}. Include Emoji: {emoji.title()}. For Social Media Platform: {platform.title()}. Language: {language}."
+
+ elif agent_slug == 'job-posting-generator':
+ job_title = input_data.get('job_title', '')
+ company_name = input_data.get('company_name', '')
+ description = input_data.get('job_description', '')
+ seniority = input_data.get('seniority_level', '')
+ contract = input_data.get('contract_type', '')
+ location = input_data.get('location', '')
+ language = input_data.get('language', 'English')
+
+ return f"Create a professional job posting for: {job_title} at {company_name}. Description: {description}. Seniority: {seniority}. Contract: {contract}. Location: {location}. Language: {language}. Make it comprehensive and attractive to candidates."
+
+ # Default formatting for other agents
+ params = [f"{key}: {value}" for key, value in input_data.items() if value]
+ return f"Execute {agent_slug.replace('-', ' ').title()} with parameters: {'. '.join(params)}."
+
+
+class AgentCompat:
+ """
+ Compatibility class to convert file-based agent data to object format
+ for templates and views that expect object attributes.
+ """
+ def __init__(self, data):
+ self.slug = data['slug']
+ self.name = data['name']
+ self.price = float(data['price'])
+ self.webhook_url = data['webhook_url']
+ self.access_url_name = data.get('access_url_name', '')
+ self.display_url_name = data.get('display_url_name', '')
+ self.id = data['slug'] # Use slug as ID for file-based agents
+ self.message_limit = data.get('message_limit', 50)
\ No newline at end of file
diff --git a/agents/views.py b/agents/views.py
index e947df3..a1f17cc 100644
--- a/agents/views.py
+++ b/agents/views.py
@@ -1,1432 +1,49 @@
-from rest_framework import status
-from rest_framework.decorators import api_view, permission_classes
-from rest_framework.permissions import IsAuthenticated
-from rest_framework.response import Response
-from rest_framework.pagination import PageNumberPagination
-from django.shortcuts import get_object_or_404, render, redirect
-from django.utils import timezone
-from django.contrib.auth.decorators import login_required
-from django.contrib import messages
-from .models import AgentExecution, ChatSession, ChatMessage
-from .serializers import AgentExecutionSerializer
-from .services import AgentFileService
-import requests
-import json
-import time
-import uuid
-import ipaddress
-from urllib.parse import urlparse
-from django.http import HttpResponse
-from reportlab.pdfgen import canvas
-from reportlab.lib.pagesizes import letter
-from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
-from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer
-from reportlab.lib.units import inch
-from io import BytesIO
+"""
+Agents views - Main imports and legacy compatibility.
+This file now imports from focused modules for better code organization.
+"""
-def validate_webhook_url(url):
- """
- Validate webhook URL to prevent SSRF attacks.
- Only allows HTTPS URLs to external, non-private networks.
- """
- try:
- parsed = urlparse(url)
-
- # Only allow HTTP/HTTPS protocols
- if parsed.scheme not in ['http', 'https']:
- raise ValueError("Only HTTP/HTTPS URLs are allowed")
-
- # Get hostname
- hostname = parsed.hostname
- if not hostname:
- raise ValueError("Invalid hostname in URL")
-
- # For localhost development, allow localhost URLs first
- if hostname in ['localhost', '127.0.0.1'] and parsed.port in [5678, 8000, 8080]:
- return True # Allow N8N development server
-
- # Check if hostname is an IP address
- try:
- ip = ipaddress.ip_address(hostname)
- # Block private, loopback, and reserved IP ranges
- if (ip.is_private or ip.is_loopback or ip.is_reserved or
- ip.is_link_local or ip.is_multicast):
- raise ValueError("Internal/private IP addresses are not allowed")
- except ValueError as e:
- if "does not appear to be an IPv4 or IPv6 address" not in str(e):
- raise # Re-raise if it's not just a "not an IP" error
- # If it's not an IP, it's a domain name - that's fine
-
- return True
-
- except Exception as e:
- raise ValueError(f"Invalid webhook URL: {str(e)}")
+# Import all views from specialized modules for backwards compatibility
+from .api_views import (
+ execute_agent,
+ execution_list,
+ execution_detail
+)
+from .chat_views import (
+ start_chat_session,
+ send_chat_message,
+ get_chat_history,
+ end_chat_session,
+ get_session_status,
+ export_chat
+)
+from .web_views import (
+ agents_marketplace,
+ agent_detail_view,
+ chat_agent_view
+)
-@api_view(['POST'])
-@permission_classes([IsAuthenticated])
-def execute_agent(request):
- """Execute an agent with provided input data"""
- agent_slug = request.data.get('agent_slug')
- input_data = request.data.get('input_data', {})
-
- if not agent_slug:
- return Response({'error': 'agent_slug is required'}, status=status.HTTP_400_BAD_REQUEST)
-
- agent_data = AgentFileService.get_agent_by_slug(agent_slug)
- if not agent_data or not agent_data.get('is_active', True):
- return Response({'error': 'Agent not found'}, status=status.HTTP_404_NOT_FOUND)
-
- agent_price = float(agent_data['price'])
-
- # Check if user has sufficient balance (using existing wallet system)
- if hasattr(request.user, 'has_sufficient_balance') and not request.user.has_sufficient_balance(agent_price):
- return Response({'error': 'Insufficient wallet balance'}, status=status.HTTP_400_BAD_REQUEST)
-
- # Create execution record
- execution = AgentExecution.objects.create(
- agent_slug=agent_data['slug'],
- agent_name=agent_data['name'],
- user=request.user,
- input_data=input_data,
- fee_charged=agent_price,
- status='pending'
- )
-
- try:
- # Deduct fee from user wallet (using existing wallet system)
- if hasattr(request.user, 'deduct_balance'):
- success = request.user.deduct_balance(
- agent_price,
- f'{agent_data["name"]} - Execution {str(execution.id)[:8]}',
- agent_data['slug']
- )
- if not success:
- execution.status = 'failed'
- execution.error_message = 'Failed to deduct wallet balance'
- execution.save()
- return Response({'error': 'Failed to deduct wallet balance'}, status=status.HTTP_400_BAD_REQUEST)
-
- # Validate webhook URL to prevent SSRF attacks
- try:
- validate_webhook_url(agent_data['webhook_url'])
- except ValueError as e:
- execution.status = 'failed'
- execution.error_message = f'Invalid webhook URL: {str(e)}'
- execution.save()
- return Response({'error': f'Invalid webhook URL: {str(e)}'}, status=status.HTTP_400_BAD_REQUEST)
-
- # Call n8n webhook with proper payload format
- execution.status = 'running'
- execution.save()
-
- # Generate session ID
- session_id = f"session_{int(time.time() * 1000)}_{str(uuid.uuid4())[:8]}"
-
- # Format message text for N8N based on agent type
- message_text = format_agent_message(agent_data['slug'], input_data)
-
- webhook_payload = {
- 'sessionId': session_id,
- 'message': {'text': message_text},
- 'webhookUrl': agent_data['webhook_url'],
- 'executionMode': 'production',
- 'agentId': agent_data['slug'],
- 'executionId': str(execution.id),
- 'userId': str(request.user.id)
- }
-
- response = requests.post(
- agent_data['webhook_url'],
- json=webhook_payload,
- timeout=90, # Increased timeout for complex processing
- headers={'Content-Type': 'application/json'}
- )
-
- # Store webhook response
- execution.webhook_response = response.json() if response.headers.get('content-type', '').startswith('application/json') else {'raw': response.text}
-
- if response.status_code == 200:
- execution.status = 'completed'
- execution.output_data = execution.webhook_response
- else:
- execution.status = 'failed'
- execution.error_message = f"Webhook returned {response.status_code}: {response.text[:500]}"
-
- execution.completed_at = timezone.now()
- execution.save()
-
- serializer = AgentExecutionSerializer(execution)
- return Response(serializer.data, status=status.HTTP_201_CREATED)
-
- except requests.RequestException as e:
- execution.status = 'failed'
- execution.error_message = str(e)
- execution.completed_at = timezone.now()
- execution.save()
-
- return Response({
- 'error': 'Failed to execute agent',
- 'execution_id': str(execution.id)
- }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
+from .direct_access_views import (
+ career_navigator_access,
+ career_navigator_view,
+ ai_brand_strategist_view,
+ ai_brand_strategist_access,
+ lean_six_sigma_expert_view,
+ lean_six_sigma_expert_access,
+ swot_analysis_expert_view,
+ swot_analysis_expert_access,
+ direct_access_handler,
+ direct_access_display
+)
+# Import utility functions for backwards compatibility
+from .utils import (
+ validate_webhook_url,
+ format_agent_message,
+ AgentCompat
+)
-def career_navigator_access(request):
- """Handle Try Now button click - charge wallet and redirect to form"""
- if not request.user.is_authenticated:
- # Clear all existing messages before adding login message
- storage = messages.get_messages(request)
- for _ in storage:
- pass # Consume all messages
- # Add login message to session for after login redirect
- request.session['post_login_message'] = 'Please complete your login to access the Career Navigator.'
- return redirect('authentication:login')
-
- # Get the career navigator agent
- agent_data = AgentFileService.get_agent_by_slug('cybersec-career-navigator')
- if not agent_data or not agent_data.get('is_active', True):
- messages.error(request, 'Career Navigator is currently unavailable.')
- return redirect('agents:marketplace')
-
- agent_price = float(agent_data['price'])
-
- # Check if user has sufficient balance
- if not request.user.has_sufficient_balance(agent_price):
- messages.error(request, f'Insufficient balance! You need {agent_price} AED to access the Career Navigator.')
- return redirect('wallet:wallet')
-
- # Deduct fee from user wallet
- success = request.user.deduct_balance(
- agent_price,
- f'{agent_data["name"]} - Direct Access',
- agent_data['slug']
- )
-
- if not success:
- messages.error(request, 'Failed to process payment. Please try again.')
- return redirect('agents:marketplace')
-
- # Create execution record for tracking
- execution = AgentExecution.objects.create(
- agent_slug=agent_data['slug'],
- agent_name=agent_data['name'],
- user=request.user,
- input_data={'action': 'direct_access', 'source': 'try_now_button'},
- fee_charged=agent_price,
- status='completed',
- output_data={
- 'type': 'direct_access',
- 'message': f'Direct access granted to {agent_data["name"]}',
- 'access_method': 'try_now_button'
- },
- completed_at=timezone.now()
- )
-
- # Redirect directly to form - no message needed
- return redirect('agents:career_navigator')
-
-
-def career_navigator_view(request):
- """Display the career navigator form page"""
- if not request.user.is_authenticated:
- # Clear all existing messages before adding login message
- storage = messages.get_messages(request)
- for _ in storage:
- pass # Consume all messages
- # Add login message to session for after login redirect
- request.session['post_login_message'] = 'Please complete your login to access the Career Navigator.'
- return redirect('authentication:login')
-
- # Get the career navigator agent
- agent_data = AgentFileService.get_agent_by_slug('cybersec-career-navigator')
- if not agent_data or not agent_data.get('is_active', True):
- messages.error(request, 'Career Navigator is currently unavailable.')
- return redirect('agents:marketplace')
-
- # Convert to compatible object
- class AgentCompat:
- def __init__(self, data):
- self.slug = data['slug']
- self.name = data['name']
- self.price = float(data['price'])
- self.webhook_url = data['webhook_url']
- self.id = data['slug']
-
- agent = AgentCompat(agent_data)
-
- # Check if user has a recent execution (within last 2 hours) or just redirect to payment
- from django.utils import timezone
- from datetime import timedelta
-
- recent_execution = AgentExecution.objects.filter(
- agent_slug=agent.slug, # Changed to slug-based lookup
- user=request.user,
- status='completed',
- created_at__gte=timezone.now() - timedelta(hours=2)
- ).first()
-
- if not recent_execution:
- messages.info(request, 'Please click "Try Now" to access your Career Navigator consultation.')
- return redirect('agents:marketplace')
-
- context = {
- 'agent': agent,
- 'form_url': agent.webhook_url,
- 'user_balance': request.user.wallet_balance,
- 'execution': recent_execution
- }
-
- return render(request, 'career_navigator.html', context)
-
-
-def ai_brand_strategist_view(request):
- """Display the AI Brand Strategist form page"""
- if not request.user.is_authenticated:
- # Clear all existing messages before adding login message
- storage = messages.get_messages(request)
- for _ in storage:
- pass # Consume all messages
- # Add login message to session for after login redirect
- request.session['post_login_message'] = 'Please complete your login to access the AI Brand Strategist.'
- return redirect('authentication:login')
-
- # Get the AI Brand Strategist agent
- agent_data = AgentFileService.get_agent_by_slug('ai-brand-strategist')
- if not agent_data or not agent_data.get('is_active', True):
- messages.error(request, 'AI Brand Strategist is currently unavailable.')
- return redirect('agents:marketplace')
-
- # Convert to compatible object
- class AgentCompat:
- def __init__(self, data):
- self.slug = data['slug']
- self.name = data['name']
- self.price = float(data['price'])
- self.webhook_url = data['webhook_url']
- self.id = data['slug']
-
- agent = AgentCompat(agent_data)
-
- # Check if user has a recent execution (within last 2 hours) or just redirect to payment
- from django.utils import timezone
- from datetime import timedelta
-
- recent_execution = AgentExecution.objects.filter(
- agent_slug=agent.slug, # Changed to slug-based lookup
- user=request.user,
- status='completed',
- created_at__gte=timezone.now() - timedelta(hours=2)
- ).first()
-
- if not recent_execution:
- messages.info(request, 'Please click "Try Now" to access your AI Brand Strategist consultation.')
- return redirect('agents:marketplace')
-
- context = {
- 'agent': agent,
- 'form_url': agent.webhook_url,
- 'user_balance': request.user.wallet_balance,
- 'execution': recent_execution
- }
-
- return render(request, 'ai_brand_strategist.html', context)
-
-
-def ai_brand_strategist_access(request):
- """Handle Try Now button click - charge wallet and redirect to form"""
- if not request.user.is_authenticated:
- # Clear all existing messages before adding login message
- storage = messages.get_messages(request)
- for _ in storage:
- pass # Consume all messages
- # Add login message to session for after login redirect
- request.session['post_login_message'] = 'Please complete your login to access the AI Brand Strategist.'
- return redirect('authentication:login')
-
- # Get the AI Brand Strategist agent
- agent_data = AgentFileService.get_agent_by_slug('ai-brand-strategist')
- if not agent_data or not agent_data.get('is_active', True):
- messages.error(request, 'AI Brand Strategist is currently unavailable.')
- return redirect('agents:marketplace')
-
- agent_price = float(agent_data['price'])
-
- # Check if user has sufficient balance
- if not request.user.has_sufficient_balance(agent_price):
- messages.error(request, f'Insufficient balance! You need {agent_price} AED to access the AI Brand Strategist.')
- return redirect('wallet:wallet')
-
- # Deduct fee from user wallet
- success = request.user.deduct_balance(
- agent_price,
- f'{agent_data["name"]} - Direct Access',
- agent_data['slug']
- )
-
- if not success:
- messages.error(request, 'Failed to process payment. Please try again.')
- return redirect('agents:marketplace')
-
- # Create execution record for tracking
- execution = AgentExecution.objects.create(
- agent_slug=agent_data['slug'],
- agent_name=agent_data['name'],
- user=request.user,
- input_data={'action': 'direct_access', 'source': 'try_now_button'},
- fee_charged=agent_price,
- status='completed',
- output_data={
- 'type': 'direct_access',
- 'message': f'Direct access granted to {agent_data["name"]}',
- 'access_method': 'try_now_button'
- },
- completed_at=timezone.now()
- )
-
- # Redirect directly to form - no message needed
- return redirect('agents:ai_brand_strategist')
-
-
-@api_view(['GET'])
-@permission_classes([IsAuthenticated])
-def execution_list(request):
- """List user's agent executions"""
- executions = AgentExecution.objects.filter(user=request.user)
-
- paginator = PageNumberPagination()
- paginator.page_size = 20
- result_page = paginator.paginate_queryset(executions, request)
- serializer = AgentExecutionSerializer(result_page, many=True)
- return paginator.get_paginated_response(serializer.data)
-
-@api_view(['GET'])
-@permission_classes([IsAuthenticated])
-def execution_detail(request, execution_id):
- """Get detailed execution information"""
- execution = get_object_or_404(AgentExecution, id=execution_id, user=request.user)
- serializer = AgentExecutionSerializer(execution)
- return Response(serializer.data)
-
-
-def format_agent_message(agent_slug, input_data):
- """Format input data into a message for N8N webhook based on agent type"""
- if agent_slug == 'social-ads-generator':
- description = input_data.get('description', '')
- platform = input_data.get('social_platform', '')
- emoji = input_data.get('include_emoji', 'yes')
- language = input_data.get('language', 'English')
-
- return f"Execute Social Media Ad Creator with the following parameters:. Describe what you'd like to generate: {description}. Include Emoji: {emoji.title()}. For Social Media Platform: {platform.title()}. Language: {language}."
-
- elif agent_slug == 'job-posting-generator':
- job_title = input_data.get('job_title', '')
- company_name = input_data.get('company_name', '')
- description = input_data.get('job_description', '')
- seniority = input_data.get('seniority_level', '')
- contract = input_data.get('contract_type', '')
- location = input_data.get('location', '')
- language = input_data.get('language', 'English')
-
- return f"Create a professional job posting for: {job_title} at {company_name}. Description: {description}. Seniority: {seniority}. Contract: {contract}. Location: {location}. Language: {language}. Make it comprehensive and attractive to candidates."
-
- # Default formatting for other agents
- params = [f"{key}: {value}" for key, value in input_data.items() if value]
- return f"Execute {agent_slug.replace('-', ' ').title()} with parameters: {'. '.join(params)}."
-
-
-# Web interface views
-@login_required
-def agent_detail_view(request, slug):
- """Render agent detail page with dynamic form or chat interface"""
- agent = AgentFileService.get_agent_by_slug(slug)
- if not agent or not agent.get('is_active', True):
- from django.http import Http404
- raise Http404("Agent not found")
-
- # Handle chat-based agents
- if agent.get('agent_type') == 'chat':
- return chat_agent_view(request, agent)
-
- # Handle form-based agents (existing behavior)
- # Get all other active agents for quick access panel
- all_agents = [a for a in AgentFileService.get_active_agents() if a['slug'] != slug]
-
- context = {
- 'agent': agent,
- 'all_agents': all_agents,
- 'timestamp': int(time.time()) # For cache busting
- }
-
- return render(request, 'agents/agent_detail.html', context)
-
-
-def agents_marketplace(request):
- """Agent marketplace view"""
- # Get agents from file service
- agents = AgentFileService.get_active_agents()
- categories = AgentFileService.get_all_categories()
-
- # Filter by category
- category_slug = request.GET.get('category')
- if category_slug:
- agents = AgentFileService.get_agents_by_category(category_slug)
-
- # Search functionality
- search_query = request.GET.get('search', '').strip()
- if search_query:
- agents = AgentFileService.search_agents(search_query)
-
- context = {
- 'agents': agents,
- 'categories': categories,
- 'selected_category': category_slug,
- 'search_query': search_query,
- 'timestamp': int(time.time())
- }
-
- return render(request, 'agents/marketplace.html', context)
-
-
-# Chat-based agent views
-def chat_agent_view(request, agent):
- """Render chat interface for chat-based agents"""
- chat_session = None
- messages = []
-
- # Convert file-based agent data to compatible object if needed
- if isinstance(agent, dict):
- class AgentCompat:
- def __init__(self, data):
- self.slug = data['slug']
- self.name = data['name']
- self.price = float(data['price'])
- self.webhook_url = data['webhook_url']
- self.id = data['slug'] # Use slug as ID for file-based agents
- self.message_limit = data.get('message_limit', 50)
-
- agent_compat = AgentCompat(agent)
- else:
- agent_compat = agent
-
- if request.user.is_authenticated:
- # Get or create active chat session (using slug-based filter for file agents)
- chat_session = ChatSession.objects.filter(
- agent_slug=agent_compat.slug, # Changed to slug-based lookup
- user=request.user,
- status='active'
- ).first()
-
- # Get session ID from URL parameter if resuming a session
- session_id = request.GET.get('session')
- if session_id and not chat_session:
- chat_session = ChatSession.objects.filter(
- session_id=session_id,
- agent_slug=agent_compat.slug, # Changed to slug-based lookup
- user=request.user
- ).first()
-
- # Get messages for the session
- if chat_session:
- messages = ChatMessage.objects.filter(session=chat_session).order_by('timestamp')
-
- # Get all other active agents for quick access panel
- all_agents = [a for a in AgentFileService.get_active_agents() if a['slug'] != agent_compat.slug]
-
- # Get previous sessions for this user and agent (excluding current active session)
- previous_sessions_query = ChatSession.objects.filter(
- agent_slug=agent_compat.slug, # Changed to slug-based lookup
- user=request.user
- ).exclude(status='active').order_by('-created_at')[:5] # Last 5 non-active sessions
-
- # Add user message count to each session
- previous_sessions = []
- for session in previous_sessions_query:
- session.user_message_count = ChatMessage.objects.filter(
- session=session,
- message_type='user'
- ).count()
- previous_sessions.append(session)
-
- # Calculate session indicators data
- session_data = {}
- if chat_session and messages.exists():
- from django.utils import timezone
- import math
-
- # Time calculations
- now = timezone.now()
- time_elapsed = now - chat_session.created_at
- time_remaining_seconds = max(0, (chat_session.expires_at - now).total_seconds())
- time_remaining_minutes = int(time_remaining_seconds // 60)
- time_remaining_hours = time_remaining_minutes // 60
- time_remaining_minutes = time_remaining_minutes % 60
-
- if time_remaining_hours > 0:
- time_remaining_str = f"{time_remaining_hours}h {time_remaining_minutes}m"
- else:
- time_remaining_str = f"{time_remaining_minutes}m"
-
- # Time percentage (how much time is left)
- total_session_time = 30 * 60 # 30 minutes in seconds
- time_percentage = max(0, min(100, (time_remaining_seconds / total_session_time) * 100))
-
- # Message calculations (only count user messages)
- user_message_count = messages.filter(message_type='user').count()
- message_limit = agent_compat.message_limit
- message_percentage = min(100, (user_message_count / message_limit) * 100)
-
- session_data = {
- 'time_remaining': time_remaining_str,
- 'time_percentage': int(time_percentage),
- 'message_count': user_message_count,
- 'message_limit': message_limit,
- 'message_percentage': int(message_percentage),
- }
-
- context = {
- 'agent': agent, # Keep original agent data for template compatibility
- 'chat_session': chat_session,
- 'messages': messages,
- 'all_agents': all_agents,
- 'previous_sessions': previous_sessions,
- 'timestamp': int(time.time()),
- **session_data # Unpack session data into context
- }
-
- return render(request, 'agents/agent_chat.html', context)
-
-
-@api_view(['POST'])
-@permission_classes([IsAuthenticated])
-def start_chat_session(request):
- """Start a new chat session"""
- agent_slug = request.data.get('agent_slug')
-
- if not agent_slug:
- return Response({'error': 'agent_slug is required'}, status=status.HTTP_400_BAD_REQUEST)
-
- agent_data = AgentFileService.get_agent_by_slug(agent_slug)
- if not agent_data or not agent_data.get('is_active', True) or agent_data.get('agent_type') != 'chat':
- return Response({'error': 'Chat agent not found'}, status=status.HTTP_404_NOT_FOUND)
-
- agent_price = float(agent_data['price'])
-
- # Check wallet balance
- if hasattr(request.user, 'wallet_balance') and request.user.wallet_balance < agent_price:
- return Response({'error': 'Insufficient wallet balance'}, status=status.HTTP_400_BAD_REQUEST)
-
- # Check for existing active session (using slug-based lookup)
- existing_session = ChatSession.objects.filter(
- agent_slug=agent_data['slug'],
- user=request.user,
- status='active'
- ).first()
-
- if existing_session:
- return Response({
- 'session_id': existing_session.session_id,
- 'message': 'Active session already exists'
- })
-
- # 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_slug=agent_data['slug'],
- agent_name=agent_data['name'],
- user=request.user,
- fee_charged=agent_price,
- status='active',
- expires_at=timezone.now() + timedelta(minutes=30)
- )
-
- # Deduct fee from wallet
- try:
- success = request.user.deduct_balance(
- agent_price,
- f'{agent_data["name"]} - Chat Session {session_id}',
- agent_data['slug']
- )
- if not success:
- # Delete the created session if payment fails
- chat_session.delete()
- return Response({
- 'error': 'Failed to process payment. Please check your wallet balance.'
- }, status=status.HTTP_400_BAD_REQUEST)
- except Exception as e:
- # Delete the created session if payment processing fails
- chat_session.delete()
- return Response({
- 'error': 'Payment processing error. Please try again.'
- }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
-
- # Send welcome message
- welcome_message = f"""## Welcome to {agent_data["name"]}! 🔍
-
-I'm here to guide you through the **5 Whys methodology** - a powerful problem-solving technique to uncover root causes.
-
-### How It Works:
-• **Ask "Why" 5 times** to drill down from symptoms to root causes
-• **Systematic analysis** of Occurrence, Detection, and Prevention
-• **Actionable insights** for effective solutions
-
-### Getting Started:
-Please describe the **specific problem** you'd like to analyze. Include:
-- What happened?
-- When did it occur?
-- What are the immediate impacts?
-
-Let's discover the root cause together! 💪"""
-
- ChatMessage.objects.create(
- session=chat_session,
- message_type='agent',
- content=welcome_message
- )
-
- return Response({
- 'session_id': chat_session.session_id,
- 'message': 'Chat session started successfully'
- })
-
-
-@api_view(['POST'])
-@permission_classes([IsAuthenticated])
-def send_chat_message(request):
- """Send a message in a chat session"""
- session_id = request.data.get('session_id')
- message_content = request.data.get('message', '').strip()
-
- if not session_id or not message_content:
- return Response({'error': 'session_id and message are required'}, status=status.HTTP_400_BAD_REQUEST)
-
- # Get chat session
- chat_session = get_object_or_404(
- ChatSession,
- session_id=session_id,
- user=request.user,
- 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)
-
- # Get agent data for message limit
- agent_data = AgentFileService.get_agent_by_slug(chat_session.agent_slug)
- message_limit = agent_data.get('message_limit', 50) if agent_data else 50
-
- # Check message limit (only count user messages)
- current_user_message_count = ChatMessage.objects.filter(session=chat_session, message_type='user').count()
- if current_user_message_count >= message_limit:
- # Auto-complete the session when message limit is reached
- chat_session.status = 'completed'
- chat_session.completed_at = timezone.now()
- chat_session.save()
-
- return Response({
- 'error': f'Message limit reached ({message_limit} messages). Session completed. You can download your conversation or start a new session.'
- }, status=status.HTTP_400_BAD_REQUEST)
-
- # Save user message
- user_message = ChatMessage.objects.create(
- session=chat_session,
- message_type='user',
- content=message_content
- )
-
- # Prepare webhook payload
- webhook_payload = {
- "message": {
- "text": f"""User message: "{message_content}"
-
-Provide helpful 5 Whys analysis guidance with professional formatting:
-
-FORMATTING REQUIREMENTS:
-- Use markdown headers (##, ###) for sections
-- Use **bold** for key terms and emphasis
-- Use bullet points (•) for lists
-- Use numbered lists (1., 2., 3.) for steps
-- Structure responses with clear sections
-- Add relevant emojis for engagement
-
-CONTENT GUIDELINES:
-- Guide through 5 Whys methodology systematically
-- Ask probing questions about Occurrence, Detection, Prevention
-- Help user drill down from symptoms to root causes
-- Keep responses conversational but structured
-- Do not generate final reports - focus on interactive guidance
-- Encourage deeper thinking with follow-up questions"""
- },
- "sessionId": session_id,
- "userId": str(request.user.id),
- "agentId": chat_session.agent_slug,
- "messageType": "chat"
- }
-
- try:
- # Get webhook URL from agent data
- webhook_url = agent_data['webhook_url'] if agent_data else None
- if not webhook_url:
- raise ValueError("Agent webhook URL not found")
-
- # Validate webhook URL
- validate_webhook_url(webhook_url)
-
- # Send to webhook
- response = requests.post(
- webhook_url,
- json=webhook_payload,
- timeout=30,
- headers={'Content-Type': 'application/json'}
- )
-
- if response.status_code == 200:
- response_data = response.json()
-
- # 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=str(agent_response),
- metadata={'webhook_response': response_data, 'raw_response': response.text}
- )
-
- # Update session timestamp and extend expiration
- chat_session.extend_session()
-
- return Response({
- 'user_message': {
- 'id': str(user_message.id),
- 'content': user_message.content,
- 'timestamp': user_message.timestamp.isoformat()
- },
- 'agent_message': {
- 'id': str(agent_message.id),
- 'content': agent_message.content,
- 'timestamp': agent_message.timestamp.isoformat()
- }
- })
- else:
- # Webhook error
- error_message = "I'm having trouble processing your message right now. Please try again."
- agent_message = ChatMessage.objects.create(
- session=chat_session,
- message_type='agent',
- content=error_message,
- metadata={'error': f'Webhook returned {response.status_code}'}
- )
-
- return Response({
- 'user_message': {
- 'id': str(user_message.id),
- 'content': user_message.content,
- 'timestamp': user_message.timestamp.isoformat()
- },
- 'agent_message': {
- 'id': str(agent_message.id),
- 'content': agent_message.content,
- 'timestamp': agent_message.timestamp.isoformat()
- }
- }, status=status.HTTP_202_ACCEPTED)
-
- except Exception as e:
- # Handle webhook errors
- error_message = "I'm experiencing technical difficulties. Please try again later."
- agent_message = ChatMessage.objects.create(
- session=chat_session,
- message_type='agent',
- content=error_message,
- metadata={'error': str(e)}
- )
-
- return Response({
- 'user_message': {
- 'id': str(user_message.id),
- 'content': user_message.content,
- 'timestamp': user_message.timestamp.isoformat()
- },
- 'agent_message': {
- 'id': str(agent_message.id),
- 'content': agent_message.content,
- 'timestamp': agent_message.timestamp.isoformat()
- }
- }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
-
-
-@api_view(['GET'])
-@permission_classes([IsAuthenticated])
-def get_chat_history(request, session_id):
- """Get chat history for a session"""
- chat_session = get_object_or_404(
- ChatSession,
- session_id=session_id,
- user=request.user
- )
-
- messages = ChatMessage.objects.filter(session=chat_session).order_by('timestamp')
-
- message_data = []
- for message in messages:
- message_data.append({
- 'id': str(message.id),
- 'message_type': message.message_type,
- 'content': message.content,
- 'timestamp': message.timestamp.isoformat()
- })
-
- return Response({
- 'session_id': session_id,
- 'status': chat_session.status,
- 'messages': message_data
- })
-
-
-@api_view(['POST'])
-@permission_classes([IsAuthenticated])
-def end_chat_session(request):
- """End a chat session"""
- session_id = request.data.get('session_id')
-
- if not session_id:
- return Response({'error': 'session_id is required'}, status=status.HTTP_400_BAD_REQUEST)
-
- chat_session = get_object_or_404(
- ChatSession,
- session_id=session_id,
- user=request.user,
- status='active'
- )
-
- chat_session.status = 'completed'
- chat_session.completed_at = timezone.now()
- chat_session.save()
-
- return Response({'message': 'Chat session ended successfully'})
-
-
-@api_view(['GET'])
-@permission_classes([IsAuthenticated])
-def get_session_status(request, session_id):
- """Get real-time session status data"""
- chat_session = get_object_or_404(
- ChatSession,
- session_id=session_id,
- user=request.user
- )
-
- from django.utils import timezone
-
- # Time calculations
- now = timezone.now()
- time_remaining_seconds = max(0, (chat_session.expires_at - now).total_seconds())
- time_remaining_minutes = int(time_remaining_seconds // 60)
- time_remaining_hours = time_remaining_minutes // 60
- time_remaining_minutes = time_remaining_minutes % 60
-
- if time_remaining_hours > 0:
- time_remaining_str = f"{time_remaining_hours}h {time_remaining_minutes}m"
- else:
- time_remaining_str = f"{time_remaining_minutes}m"
-
- # Time percentage (how much time is left)
- total_session_time = 30 * 60 # 30 minutes in seconds
- time_percentage = max(0, min(100, (time_remaining_seconds / total_session_time) * 100))
-
- # Get agent data for message limit
- agent_data = AgentFileService.get_agent_by_slug(chat_session.agent_slug)
- message_limit = agent_data.get('message_limit', 50) if agent_data else 50
-
- # Message calculations (only count user messages)
- message_count = ChatMessage.objects.filter(session=chat_session, message_type='user').count()
- message_percentage = min(100, (message_count / message_limit) * 100)
-
- return Response({
- 'success': True,
- 'session_id': session_id,
- 'status': chat_session.status,
- 'time_remaining_seconds': int(time_remaining_seconds),
- 'time_remaining_str': time_remaining_str,
- 'time_percentage': int(time_percentage),
- 'message_count': message_count,
- 'message_limit': message_limit,
- 'message_percentage': int(message_percentage),
- 'is_expired': chat_session.is_expired()
- })
-
-
-@login_required
-def export_chat(request, session_id):
- """Export chat session as PDF or TXT"""
- format_type = request.GET.get('format', 'pdf').lower()
-
- # Get chat session and verify ownership
- chat_session = get_object_or_404(
- ChatSession,
- session_id=session_id,
- user=request.user
- )
-
- # Get all messages for this session
- messages = ChatMessage.objects.filter(session=chat_session).order_by('timestamp')
-
- if not messages.exists():
- return HttpResponse('No messages found in this chat session.', status=404)
-
- if format_type == 'pdf':
- return export_chat_pdf(chat_session, messages)
- elif format_type == 'txt':
- return export_chat_txt(chat_session, messages)
- else:
- return HttpResponse('Invalid format. Use pdf or txt.', status=400)
-
-
-def export_chat_pdf(chat_session, messages):
- """Generate PDF export of chat session"""
- buffer = BytesIO()
- doc = SimpleDocTemplate(buffer, pagesize=letter)
- styles = getSampleStyleSheet()
- story = []
-
- # Title
- title_style = ParagraphStyle(
- 'CustomTitle',
- parent=styles['Heading1'],
- fontSize=18,
- spaceAfter=30,
- alignment=1 # Center alignment
- )
-
- story.append(Paragraph(f"5 Whys Analysis - {chat_session.agent_name}", title_style))
- story.append(Spacer(1, 12))
-
- # Session info
- info_style = styles['Normal']
- story.append(Paragraph(f"Session ID: {chat_session.session_id}", info_style))
- story.append(Paragraph(f"Date: {chat_session.created_at.strftime('%B %d, %Y at %I:%M %p')}", info_style))
- story.append(Paragraph(f"Agent: {chat_session.agent_name}", info_style))
- story.append(Paragraph(f"Total Messages: {messages.count()}", info_style))
- story.append(Spacer(1, 20))
-
- # Messages
- user_style = ParagraphStyle(
- 'UserMessage',
- parent=styles['Normal'],
- leftIndent=0,
- rightIndent=50,
- spaceBefore=12,
- spaceAfter=6,
- fontSize=10
- )
-
- agent_style = ParagraphStyle(
- 'AgentMessage',
- parent=styles['Normal'],
- leftIndent=50,
- rightIndent=0,
- spaceBefore=12,
- spaceAfter=6,
- fontSize=10
- )
-
- for message in messages:
- timestamp = message.timestamp.strftime('%I:%M %p')
-
- if message.message_type == 'user':
- story.append(Paragraph(f"You ({timestamp}):
{message.content}", user_style))
- elif message.message_type == 'agent':
- story.append(Paragraph(f"{chat_session.agent_name} ({timestamp}):
{message.content}", agent_style))
- elif message.message_type == 'system':
- story.append(Paragraph(f"System ({timestamp}): {message.content}", styles['Normal']))
-
- # Build PDF
- doc.build(story)
- buffer.seek(0)
-
- response = HttpResponse(buffer.getvalue(), content_type='application/pdf')
- response['Content-Disposition'] = f'attachment; filename="5whys_chat_{chat_session.session_id}.pdf"'
- return response
-
-
-def export_chat_txt(chat_session, messages):
- """Generate TXT export of chat session"""
- content = []
- content.append("=" * 60)
- content.append(f"5 Whys Analysis - {chat_session.agent_name}")
- content.append("=" * 60)
- content.append("")
- content.append(f"Session ID: {chat_session.session_id}")
- content.append(f"Date: {chat_session.created_at.strftime('%B %d, %Y at %I:%M %p')}")
- content.append(f"Agent: {chat_session.agent_name}")
- content.append(f"Total Messages: {messages.count()}")
- content.append("")
- content.append("-" * 60)
- content.append("CONVERSATION")
- content.append("-" * 60)
- content.append("")
-
- for message in messages:
- timestamp = message.timestamp.strftime('%I:%M %p')
-
- if message.message_type == 'user':
- content.append(f"You ({timestamp}):")
- content.append(message.content)
- elif message.message_type == 'agent':
- content.append(f"{chat_session.agent_name} ({timestamp}):")
- content.append(message.content)
- elif message.message_type == 'system':
- content.append(f"System ({timestamp}): {message.content}")
-
- content.append("") # Empty line between messages
-
- content.append("-" * 60)
- content.append("End of Conversation")
- content.append("-" * 60)
-
- text_content = "\n".join(content)
-
- response = HttpResponse(text_content, content_type='text/plain')
- response['Content-Disposition'] = f'attachment; filename="5whys_chat_{chat_session.session_id}.txt"'
- return response
-
-
-# Generic Direct Access Views for External Form Agents
-@login_required
-def direct_access_handler(request, slug):
- """
- Generic handler for direct access agents (external forms like JotForm).
- Handles payment processing and grants access to external form.
- """
- agent_data = AgentFileService.get_agent_by_slug(slug)
- if not agent_data or not agent_data.get('is_active', True):
- from django.http import Http404
- raise Http404("Agent not found")
-
- # Convert to compatible object
- class AgentCompat:
- def __init__(self, data):
- self.slug = data['slug']
- self.name = data['name']
- self.price = float(data['price'])
- self.webhook_url = data['webhook_url']
- self.access_url_name = data.get('access_url_name', '')
- self.display_url_name = data.get('display_url_name', '')
- self.id = data['slug']
-
- agent = AgentCompat(agent_data)
-
- # Verify this is a direct access agent
- if not agent.access_url_name or not agent.display_url_name:
- messages.error(request, 'This agent does not support direct access.')
- return redirect('agents:marketplace')
-
- # Handle payment for paid agents
- if agent_price > 0:
- user_balance = request.user.wallet_balance
- if user_balance < agent_price:
- messages.error(request, f'Insufficient balance. You need {agent_price} AED but have {user_balance} AED.')
- return redirect('wallet:wallet')
-
- # Process payment
- try:
- from wallet.models import WalletTransaction
- WalletTransaction.objects.create(
- user=request.user,
- amount=-agent_price,
- type='agent_usage',
- description=f'Payment for {agent.name}',
- agent_slug=agent.slug
- )
- except Exception as e:
- messages.error(request, 'Payment processing failed. Please try again.')
- return redirect('agents:agent_detail', slug=slug)
-
- # Grant access - redirect directly to display page
- return redirect('agents:direct_access_display', slug=slug)
-
-
-@login_required
-def direct_access_display(request, slug):
- """
- Generic display handler for direct access agents.
- Shows external form (JotForm, Google Forms, etc.) in iframe or redirects directly.
- """
- agent_data = AgentFileService.get_agent_by_slug(slug)
- if not agent_data or not agent_data.get('is_active', True):
- from django.http import Http404
- raise Http404("Agent not found")
-
- # Convert to compatible object
- class AgentCompat:
- def __init__(self, data):
- self.slug = data['slug']
- self.name = data['name']
- self.price = float(data['price'])
- self.webhook_url = data['webhook_url']
- self.access_url_name = data.get('access_url_name', '')
- self.display_url_name = data.get('display_url_name', '')
- self.id = data['slug']
-
- agent = AgentCompat(agent_data)
-
- # Verify this is a direct access agent
- if not agent.access_url_name or not agent.display_url_name:
- messages.error(request, 'This agent does not support direct access.')
- return redirect('agents:marketplace')
-
- # For now, redirect directly to external form
- # Future: Can render iframe template or custom display logic
- return redirect(agent.webhook_url)
-
-
-def lean_six_sigma_expert_view(request):
- """Display the Lean Six Sigma Expert form page"""
- if not request.user.is_authenticated:
- # Clear all existing messages before adding login message
- storage = messages.get_messages(request)
- for _ in storage:
- pass # Consume all messages
- # Add login message to session for after login redirect
- request.session['post_login_message'] = 'Please complete your login to access the Lean Six Sigma Expert.'
- return redirect('authentication:login')
-
- # Get the Lean Six Sigma Expert agent
- agent_data = AgentFileService.get_agent_by_slug('lean-six-sigma-expert')
- if not agent_data or not agent_data.get('is_active', True):
- messages.error(request, 'Lean Six Sigma Expert is currently unavailable.')
- return redirect('agents:marketplace')
-
- # Convert to compatible object
- class AgentCompat:
- def __init__(self, data):
- self.slug = data['slug']
- self.name = data['name']
- self.price = float(data['price'])
- self.webhook_url = data['webhook_url']
- self.id = data['slug']
-
- agent = AgentCompat(agent_data)
-
- # Check if user has a recent execution (within last 2 hours) or just redirect to payment
- from django.utils import timezone
- from datetime import timedelta
-
- recent_execution = AgentExecution.objects.filter(
- agent_slug=agent.slug, # Changed to slug-based lookup
- user=request.user,
- status='completed',
- created_at__gte=timezone.now() - timedelta(hours=2)
- ).first()
-
- if not recent_execution:
- messages.info(request, 'Please click "Try Now" to access your Lean Six Sigma Expert consultation.')
- return redirect('agents:marketplace')
-
- context = {
- 'agent': agent,
- 'form_url': agent.webhook_url,
- 'user_balance': request.user.wallet_balance,
- 'execution': recent_execution
- }
-
- return render(request, 'lean_six_sigma_expert.html', context)
-
-
-def lean_six_sigma_expert_access(request):
- """Handle Try Now button click - charge wallet and redirect to form"""
- if not request.user.is_authenticated:
- # Clear all existing messages before adding login message
- storage = messages.get_messages(request)
- for _ in storage:
- pass # Consume all messages
- # Add login message to session for after login redirect
- request.session['post_login_message'] = 'Please complete your login to access the Lean Six Sigma Expert.'
- return redirect('authentication:login')
-
- # Get the Lean Six Sigma Expert agent
- agent_data = AgentFileService.get_agent_by_slug('lean-six-sigma-expert')
- if not agent_data or not agent_data.get('is_active', True):
- messages.error(request, 'Lean Six Sigma Expert is currently unavailable.')
- return redirect('agents:marketplace')
-
- agent_price = float(agent_data['price'])
-
- # Check if user has sufficient balance
- if not request.user.has_sufficient_balance(agent_price):
- messages.error(request, f'Insufficient balance! You need {agent_price} AED to access the Lean Six Sigma Expert.')
- return redirect('wallet:wallet')
-
- # Deduct fee from user wallet
- success = request.user.deduct_balance(
- agent_price,
- f'{agent_data["name"]} - Direct Access',
- agent_data['slug']
- )
-
- if not success:
- messages.error(request, 'Failed to process payment. Please try again.')
- return redirect('agents:marketplace')
-
- # Create execution record for tracking
- execution = AgentExecution.objects.create(
- agent_slug=agent_data['slug'],
- agent_name=agent_data['name'],
- user=request.user,
- input_data={'action': 'direct_access', 'source': 'try_now_button'},
- fee_charged=agent_price,
- status='completed',
- output_data={
- 'type': 'direct_access',
- 'message': f'Direct access granted to {agent_data["name"]}',
- 'access_method': 'try_now_button'
- },
- completed_at=timezone.now()
- )
-
- # Redirect directly to form - no message needed
- return redirect('agents:lean_six_sigma_expert')
-
-
-def swot_analysis_expert_view(request):
- """Display the SWOT Analysis Expert form page"""
- if not request.user.is_authenticated:
- # Clear all existing messages before adding login message
- storage = messages.get_messages(request)
- for _ in storage:
- pass # Consume all messages
- # Add login message to session for after login redirect
- request.session['post_login_message'] = 'Please complete your login to access the SWOT Analysis Expert.'
- return redirect('authentication:login')
-
- # Get the SWOT Analysis Expert agent
- agent_data = AgentFileService.get_agent_by_slug('swot-analysis-expert')
- if not agent_data or not agent_data.get('is_active', True):
- messages.error(request, 'SWOT Analysis Expert is currently unavailable.')
- return redirect('agents:marketplace')
-
- # Convert to compatible object
- class AgentCompat:
- def __init__(self, data):
- self.slug = data['slug']
- self.name = data['name']
- self.price = float(data['price'])
- self.webhook_url = data['webhook_url']
- self.id = data['slug']
-
- agent = AgentCompat(agent_data)
-
- # Check if user has a recent execution (within last 2 hours) or just redirect to payment
- from django.utils import timezone
- from datetime import timedelta
-
- recent_execution = AgentExecution.objects.filter(
- agent_slug=agent.slug, # Changed to slug-based lookup
- user=request.user,
- status='completed',
- created_at__gte=timezone.now() - timedelta(hours=2)
- ).first()
-
- if not recent_execution:
- messages.info(request, 'Please click "Try Now" to access your SWOT Analysis Expert consultation.')
- return redirect('agents:marketplace')
-
- context = {
- 'agent': agent,
- 'form_url': agent.webhook_url,
- 'user_balance': request.user.wallet_balance,
- 'execution': recent_execution
- }
-
- return render(request, 'swot_analysis_expert.html', context)
-
-
-def swot_analysis_expert_access(request):
- """Handle Try Now button click - charge wallet and redirect to form"""
- if not request.user.is_authenticated:
- # Clear all existing messages before adding login message
- storage = messages.get_messages(request)
- for _ in storage:
- pass # Consume all messages
- # Add login message to session for after login redirect
- request.session['post_login_message'] = 'Please complete your login to access the SWOT Analysis Expert.'
- return redirect('authentication:login')
-
- # Get the SWOT Analysis Expert agent
- agent_data = AgentFileService.get_agent_by_slug('swot-analysis-expert')
- if not agent_data or not agent_data.get('is_active', True):
- messages.error(request, 'SWOT Analysis Expert is currently unavailable.')
- return redirect('agents:marketplace')
-
- agent_price = float(agent_data['price'])
-
- # Check if user has sufficient balance
- if not request.user.has_sufficient_balance(agent_price):
- messages.error(request, f'Insufficient balance! You need {agent_price} AED to access the SWOT Analysis Expert.')
- return redirect('wallet:wallet')
-
- # Deduct fee from user wallet
- success = request.user.deduct_balance(
- agent_price,
- f'{agent_data["name"]} - Direct Access',
- agent_data['slug']
- )
-
- if not success:
- messages.error(request, 'Failed to process payment. Please try again.')
- return redirect('agents:marketplace')
-
- # Create execution record for tracking
- execution = AgentExecution.objects.create(
- agent_slug=agent_data['slug'],
- agent_name=agent_data['name'],
- user=request.user,
- input_data={'action': 'direct_access', 'source': 'try_now_button'},
- fee_charged=agent_price,
- status='completed',
- output_data={
- 'type': 'direct_access',
- 'message': f'Direct access granted to {agent_data["name"]}',
- 'access_method': 'try_now_button'
- },
- completed_at=timezone.now()
- )
-
- # Redirect directly to form - no message needed
- return redirect('agents:swot_analysis_expert')
+# All functionality is now available through focused modules
+# This maintains backwards compatibility while improving code organization
\ No newline at end of file
diff --git a/agents/web_views.py b/agents/web_views.py
new file mode 100644
index 0000000..bc8743f
--- /dev/null
+++ b/agents/web_views.py
@@ -0,0 +1,162 @@
+"""
+Web interface views for the agents app.
+Handles template rendering for marketplace, agent detail pages, and chat interfaces.
+"""
+
+from django.shortcuts import render
+from django.contrib.auth.decorators import login_required
+from .services import AgentFileService
+from .models import ChatSession, ChatMessage
+from .utils import AgentCompat
+import time
+
+
+def agents_marketplace(request):
+ """Agent marketplace view"""
+ # Get agents from file service
+ agents = AgentFileService.get_active_agents()
+ categories = AgentFileService.get_all_categories()
+
+ # Filter by category
+ category_slug = request.GET.get('category')
+ if category_slug:
+ agents = AgentFileService.get_agents_by_category(category_slug)
+
+ # Search functionality
+ search_query = request.GET.get('search', '').strip()
+ if search_query:
+ agents = AgentFileService.search_agents(search_query)
+
+ context = {
+ 'agents': agents,
+ 'categories': categories,
+ 'selected_category': category_slug,
+ 'search_query': search_query,
+ 'timestamp': int(time.time())
+ }
+
+ return render(request, 'agents/marketplace.html', context)
+
+
+@login_required
+def agent_detail_view(request, slug):
+ """Render agent detail page with dynamic form or chat interface"""
+ agent = AgentFileService.get_agent_by_slug(slug)
+ if not agent or not agent.get('is_active', True):
+ from django.http import Http404
+ raise Http404("Agent not found")
+
+ # Handle chat-based agents
+ if agent.get('agent_type') == 'chat':
+ return chat_agent_view(request, agent)
+
+ # Handle form-based agents (existing behavior)
+ # Get all other active agents for quick access panel
+ all_agents = [a for a in AgentFileService.get_active_agents() if a['slug'] != slug]
+
+ context = {
+ 'agent': agent,
+ 'all_agents': all_agents,
+ 'timestamp': int(time.time()) # For cache busting
+ }
+
+ return render(request, 'agents/agent_detail.html', context)
+
+
+def chat_agent_view(request, agent):
+ """Render chat interface for chat-based agents"""
+ chat_session = None
+ messages = []
+
+ # Convert file-based agent data to compatible object if needed
+ if isinstance(agent, dict):
+ agent_compat = AgentCompat(agent)
+ else:
+ agent_compat = agent
+
+ if request.user.is_authenticated:
+ # Get or create active chat session (using slug-based filter for file agents)
+ chat_session = ChatSession.objects.filter(
+ agent_slug=agent_compat.slug, # Changed to slug-based lookup
+ user=request.user,
+ status='active'
+ ).first()
+
+ # Get session ID from URL parameter if resuming a session
+ session_id = request.GET.get('session')
+ if session_id and not chat_session:
+ chat_session = ChatSession.objects.filter(
+ session_id=session_id,
+ agent_slug=agent_compat.slug, # Changed to slug-based lookup
+ user=request.user
+ ).first()
+
+ # Get messages for the session
+ if chat_session:
+ messages = ChatMessage.objects.filter(session=chat_session).order_by('timestamp')
+
+ # Get all other active agents for quick access panel
+ all_agents = [a for a in AgentFileService.get_active_agents() if a['slug'] != agent_compat.slug]
+
+ # Get previous sessions for this user and agent (excluding current active session)
+ previous_sessions_query = ChatSession.objects.filter(
+ agent_slug=agent_compat.slug, # Changed to slug-based lookup
+ user=request.user
+ ).exclude(status='active').order_by('-created_at')[:5] # Last 5 non-active sessions
+
+ # Add user message count to each session
+ previous_sessions = []
+ for session in previous_sessions_query:
+ session.user_message_count = ChatMessage.objects.filter(
+ session=session,
+ message_type='user'
+ ).count()
+ previous_sessions.append(session)
+
+ # Calculate session indicators data
+ session_data = {}
+ if chat_session and messages.exists():
+ from django.utils import timezone
+ import math
+
+ # Time calculations
+ now = timezone.now()
+ time_elapsed = now - chat_session.created_at
+ time_remaining_seconds = max(0, (chat_session.expires_at - now).total_seconds())
+ time_remaining_minutes = int(time_remaining_seconds // 60)
+ time_remaining_hours = time_remaining_minutes // 60
+ time_remaining_minutes = time_remaining_minutes % 60
+
+ if time_remaining_hours > 0:
+ time_remaining_str = f"{time_remaining_hours}h {time_remaining_minutes}m"
+ else:
+ time_remaining_str = f"{time_remaining_minutes}m"
+
+ # Time percentage (how much time is left)
+ total_session_time = 30 * 60 # 30 minutes in seconds
+ time_percentage = max(0, min(100, (time_remaining_seconds / total_session_time) * 100))
+
+ # Message calculations (only count user messages)
+ user_message_count = messages.filter(message_type='user').count()
+ message_limit = agent_compat.message_limit
+ message_percentage = min(100, (user_message_count / message_limit) * 100)
+
+ session_data = {
+ 'time_remaining': time_remaining_str,
+ 'time_percentage': int(time_percentage),
+ 'message_count': user_message_count,
+ 'message_limit': message_limit,
+ 'message_percentage': int(message_percentage),
+ }
+
+ context = {
+ 'agent': agent, # Keep original agent data for template compatibility
+ 'chat_session': chat_session,
+ 'messages': messages,
+ 'all_agents': all_agents,
+ 'previous_sessions': previous_sessions,
+ 'timestamp': int(time.time()),
+ **session_data # Unpack session data into context
+ }
+
+ return render(request, 'agents/agent_chat.html', context)
\ No newline at end of file