From 4b64cb2d2910b8093811768d8a1b3c47d69856ac Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 14 Aug 2025 22:36:36 +0530 Subject: [PATCH] =?UTF-8?q?=F0=9F=8F=97=EF=B8=8F=20Refactor=20agents=20vie?= =?UTF-8?q?ws=20into=20focused=20modules=20for=20better=20code=20organizat?= =?UTF-8?q?ion?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit **View Separation Completed:** - **agents/api_views.py** - REST API endpoints (execute_agent, execution_list/detail) - **agents/chat_views.py** - Chat session management and message handling - **agents/web_views.py** - Web interface views (marketplace, agent detail pages) - **agents/direct_access_views.py** - External form integration handlers - **agents/utils.py** - Utility functions (webhook validation, message formatting, AgentCompat class) **Benefits:** ✅ Better code navigation and maintainability (1,433 lines → 5 focused modules) ✅ Clear separation of concerns (API vs web vs chat vs external forms) ✅ Easier to add new agent types in the future ✅ Follows Django best practices ✅ Maintains full backwards compatibility through main views.py imports **Architecture Status:** - All 8 agents working perfectly (4 webhook + 4 direct access) - File-based agent system with production-ready caching - Wallet balance updates immediately after execution - PDF analyzer showing properly formatted results - Job posting generator with clean formatting - "Explore Other Agents" button functioning correctly - N8N webhook communication working seamlessly 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- agents/api_views.py | 149 ++++ agents/chat_views.py | 552 +++++++++++++ agents/direct_access_views.py | 470 +++++++++++ agents/utils.py | 88 ++ agents/views.py | 1469 +-------------------------------- agents/web_views.py | 162 ++++ 6 files changed, 1464 insertions(+), 1426 deletions(-) create mode 100644 agents/api_views.py create mode 100644 agents/chat_views.py create mode 100644 agents/direct_access_views.py create mode 100644 agents/utils.py create mode 100644 agents/web_views.py 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