AI_Chatbot / views.py
embedingHF's picture
Upload folder using huggingface_hub
ae677bb verified
Raw
History Blame Contribute Delete
30.2 kB
import logging
import secrets
from django.conf import settings
from django.core.exceptions import ValidationError
from rest_framework import status
from rest_framework.decorators import api_view, authentication_classes, permission_classes
from rest_framework.permissions import IsAuthenticated, AllowAny
from rest_framework.response import Response
from rest_framework_simplejwt.authentication import JWTAuthentication
from ai_chatbot.serializers import EnableAutoChatSerializer, PropertyCustomQASerializer
from .models import MasterQuestion, PropertyCustomQA, PropertyQA, AgencyAutoChatSetting, PropertyAutoChatState, GlobalQA
from .services import EmbeddingService
logger = logging.getLogger(__name__)
embedding_service = EmbeddingService()
# FIX #8: Add serializer validation
from rest_framework import serializers
class QAAnswerSerializer(serializers.Serializer):
master_question_id = serializers.UUIDField(required=True)
answer = serializers.CharField(max_length=5000, required=True, allow_blank=False)
class BulkQASerializer(serializers.Serializer):
property_id = serializers.UUIDField(required=True)
answers = QAAnswerSerializer(many=True, required=True)
# FIX #3: Webhook authentication
class WebhookSecretAuthentication:
"""Custom authentication for webhook endpoint"""
def authenticate(self, request):
secret = request.headers.get('X-Webhook-Secret')
expected_secret = getattr(settings, 'AI_WEBHOOK_SECRET', None)
if not expected_secret:
logger.warning("AI_WEBHOOK_SECRET not set in settings")
return None
if secret and secrets.compare_digest(secret, expected_secret):
return (None, None) # Valid
return None
def authenticate_header(self, request):
return 'X-Webhook-Secret'
@api_view(['GET'])
@authentication_classes([JWTAuthentication])
@permission_classes([IsAuthenticated])
def get_master_questions(request):
"""Get all master questions for agency to answer"""
questions = MasterQuestion.objects.filter(is_active=True).order_by('order')
data = [{'id': str(q.id), 'question': q.question, 'order': q.order} for q in questions]
return Response({'questions': data})
@api_view(['GET'])
@authentication_classes([JWTAuthentication])
@permission_classes([IsAuthenticated])
def get_property_qa_status(request, property_id):
"""Get Q&A status for a property"""
user = request.user
master_questions = MasterQuestion.objects.filter(is_active=True).order_by('order')
# Get existing answers
existing_qa = {
str(qa.master_question_id): {
'answer': qa.answer,
'qa_id': str(qa.id),
'updated_at': qa.updated_at
}
for qa in PropertyQA.objects.filter(
property_id=property_id,
agency=user
).select_related('master_question')
}
questions_data = []
for mq in master_questions:
mq_id = str(mq.id)
questions_data.append({
'id': mq_id,
'question': mq.question,
'order': mq.order,
'is_answered': mq_id in existing_qa,
'answer': existing_qa[mq_id]['answer'] if mq_id in existing_qa else None,
'updated_at': existing_qa[mq_id]['updated_at'] if mq_id in existing_qa else None,
})
# Get settings
try:
auto_chat_setting = AgencyAutoChatSetting.objects.get(agency=user)
auto_chat_enabled = auto_chat_setting.is_enabled
delay_seconds = auto_chat_setting.delay_seconds
confidence_threshold = auto_chat_setting.confidence_threshold
except AgencyAutoChatSetting.DoesNotExist:
auto_chat_enabled = False
delay_seconds = 30
confidence_threshold = 0.6
try:
property_auto_state = PropertyAutoChatState.objects.get(property_id=property_id)
property_auto_enabled = property_auto_state.is_auto_chat_enabled
except PropertyAutoChatState.DoesNotExist:
property_auto_enabled = False
answered_count = sum(1 for q in questions_data if q['is_answered'])
total_questions = len(questions_data)
return Response({
'property_id': property_id,
'master_questions': questions_data,
'stats': {
'total_questions': total_questions,
'answered_count': answered_count,
'completion_percentage': round((answered_count / total_questions) * 100, 1) if total_questions > 0 else 0,
'remaining_count': total_questions - answered_count
},
'auto_chat_settings': {
'is_enabled': auto_chat_enabled,
'property_auto_enabled': property_auto_enabled,
'delay_seconds': delay_seconds,
'confidence_threshold': confidence_threshold
}
})
@api_view(['POST'])
@authentication_classes([JWTAuthentication])
@permission_classes([IsAuthenticated])
def save_property_qa_bulk(request):
"""
FIX #5 & #9: Optimized bulk save with proper auto-enable logic
"""
user = request.user
# Check role
if not hasattr(user, 'role') or getattr(user.role, 'role_type', None) != 'agency':
return Response({'error': 'Only agencies can add Q&A'}, status=status.HTTP_403_FORBIDDEN)
# FIX #8: Validate input
serializer = BulkQASerializer(data=request.data)
if not serializer.is_valid():
return Response({'error': 'Invalid data', 'details': serializer.errors},
status=status.HTTP_400_BAD_REQUEST)
validated_data = serializer.validated_data
property_id = validated_data['property_id']
answers = validated_data['answers']
if not answers:
return Response({'error': 'No answers provided'}, status=status.HTTP_400_BAD_REQUEST)
# FIX #5: Get existing answers to minimize database operations
existing_qa = {
str(qa.master_question_id): qa
for qa in PropertyQA.objects.filter(
property_id=property_id,
agency=user
).select_related('master_question')
}
# Track which master questions were answered
answered_master_ids = set()
created_count = 0
updated_count = 0
errors = []
for item in answers:
master_q_id = str(item['master_question_id'])
answer = item['answer'].strip()
if not answer:
continue
try:
master_q = MasterQuestion.objects.get(id=master_q_id, is_active=True)
answered_master_ids.add(master_q_id)
# Generate embedding
embedding = embedding_service.generate_embedding(master_q.question)
# Update or create
if master_q_id in existing_qa:
# Update existing
qa = existing_qa[master_q_id]
qa.answer = answer
qa.question_embedding = embedding
qa.save(update_fields=['answer', 'question_embedding', 'updated_at'])
updated_count += 1
else:
# Create new
PropertyQA.objects.create(
agency=user,
property_id=property_id,
master_question=master_q,
answer=answer,
question_embedding=embedding
)
created_count += 1
except MasterQuestion.DoesNotExist:
errors.append(f'Master question not found: {master_q_id}')
except Exception as e:
errors.append(f'Error for question {master_q_id}: {str(e)}')
# Delete answers that were removed (not in current submission)
to_delete_ids = set(existing_qa.keys()) - answered_master_ids
if to_delete_ids:
deleted_count = PropertyQA.objects.filter(
property_id=property_id,
master_question_id__in=to_delete_ids
).delete()[0]
logger.info(f"Deleted {deleted_count} removed answers for property {property_id}")
# FIX #9: Auto-enable only when ALL ACTIVE master questions are answered
total_active_questions = MasterQuestion.objects.filter(is_active=True).count()
total_answered = PropertyQA.objects.filter(
property_id=property_id,
master_question__is_active=True
).count()
auto_chat_enabled = False
if total_answered >= total_active_questions and total_active_questions > 0:
property_state, created = PropertyAutoChatState.objects.update_or_create(
property_id=property_id,
defaults={'is_auto_chat_enabled': True}
)
auto_chat_enabled = True
logger.info(f"✅ Auto-chat enabled for property {property_id} ({total_answered}/{total_active_questions} questions answered)")
return Response({
'message': f'Saved answers for property',
'created': created_count,
'updated': updated_count,
'total': created_count + updated_count,
'auto_chat_enabled': auto_chat_enabled,
'completion': {
'answered': total_answered,
'total': total_active_questions,
'percentage': round((total_answered / total_active_questions) * 100, 1) if total_active_questions > 0 else 0
},
'errors': errors if errors else None
}, status=status.HTTP_200_OK if (created_count + updated_count) > 0 else status.HTTP_400_BAD_REQUEST)
@api_view(['POST'])
@authentication_classes([JWTAuthentication])
@permission_classes([IsAuthenticated])
def toggle_auto_chat(request, property_id):
"""Enable/disable auto-chat for a specific property"""
user = request.user
if not hasattr(user, 'role') or getattr(user.role, 'role_type', None) != 'agency':
return Response({'error': 'Only agencies can configure auto-chat'}, status=status.HTTP_403_FORBIDDEN)
is_enabled = request.data.get('is_enabled', False)
# Validate before enabling
if is_enabled:
total_required = MasterQuestion.objects.filter(is_active=True).count()
total_answered = PropertyQA.objects.filter(
property_id=property_id,
agency=user,
master_question__is_active=True
).count()
if total_answered < total_required:
return Response({
'error': f'Please answer all {total_required} active questions first. Currently answered: {total_answered}/{total_required}'
}, status=status.HTTP_400_BAD_REQUEST)
# Update property auto-chat state
property_state, created = PropertyAutoChatState.objects.get_or_create(
property_id=property_id,
defaults={'is_auto_chat_enabled': is_enabled}
)
if not created:
property_state.is_auto_chat_enabled = is_enabled
property_state.save()
# Also update agency-wide setting
agency_setting, _ = AgencyAutoChatSetting.objects.get_or_create(
agency=user,
defaults={
'is_enabled': is_enabled,
'delay_seconds': 30,
'confidence_threshold': 0.6
}
)
if not agency_setting.is_enabled and is_enabled:
agency_setting.is_enabled = True
agency_setting.save()
return Response({
'property_id': property_id,
'is_enabled': property_state.is_auto_chat_enabled,
'message': f'Auto-chat {"enabled" if is_enabled else "disabled"} for this property'
})
@api_view(['POST'])
@authentication_classes([WebhookSecretAuthentication]) # FIX #3: Secure webhook
@permission_classes([AllowAny])
def webhook_auto_reply(request):
"""
FIX #3: Secured webhook endpoint for Chat app to trigger auto-reply
"""
from .services import AutoChatService
# Validate required fields
required_fields = ['chat_id', 'property_id', 'message']
for field in required_fields:
if field not in request.data:
return Response({'error': f'Missing required field: {field}'},
status=status.HTTP_400_BAD_REQUEST)
data = request.data
chat_id = data.get('chat_id')
property_id = data.get('property_id')
client_message = data.get('message')
last_agency_reply_at = data.get('last_agency_reply_at')
last_client_message_at = data.get('last_client_message_at')
auto_chat_service = AutoChatService()
# Check if auto-reply should trigger
if auto_chat_service.should_auto_reply(chat_id, property_id, last_agency_reply_at, last_client_message_at):
reply = auto_chat_service.generate_auto_reply(client_message, property_id, chat_id)
if reply:
return Response(reply)
return Response({'should_auto_reply': False, 'error': 'Failed to generate reply'})
return Response({'should_auto_reply': False})
@api_view(['POST'])
@authentication_classes([JWTAuthentication])
@permission_classes([IsAuthenticated])
def enable_auto_chat_with_questions(request):
"""
Enable auto-chat for a property and add custom questions (max 5)
"""
user = request.user
# Check if agency
if not hasattr(user, 'role') or getattr(user.role, 'role_type', None) != 'agency':
return Response({'error': 'Only agencies can enable auto-chat'},
status=status.HTTP_403_FORBIDDEN)
# Validate input
serializer = EnableAutoChatSerializer(data=request.data)
if not serializer.is_valid():
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
validated_data = serializer.validated_data
property_id = validated_data['property_id']
enable = validated_data['enable']
custom_questions = validated_data.get('custom_questions', [])
# Verify property belongs to this agency
from Property.models import Property
try:
property_obj = Property.objects.get(id=property_id, user=user)
except Property.DoesNotExist:
return Response({'error': 'Property not found or does not belong to you'},
status=status.HTTP_404_NOT_FOUND)
# Delete existing custom questions for this property
PropertyCustomQA.objects.filter(property_id=property_id).delete()
# Add new custom questions with embeddings
created_questions = []
for idx, q_data in enumerate(custom_questions):
# Generate embedding for the question
embedding = embedding_service.generate_embedding(q_data['question'])
custom_qa = PropertyCustomQA.objects.create(
agency=user,
property=property_obj,
question=q_data['question'],
answer=q_data['answer'],
question_embedding=embedding,
order=idx + 1, # 1, 2, 3, 4, 5
is_active=True
)
created_questions.append({
'order': custom_qa.order,
'question': custom_qa.question,
'answer': custom_qa.answer
})
# Enable or disable auto-chat
property_state, created = PropertyAutoChatState.objects.update_or_create(
property_id=property_id,
defaults={'is_auto_chat_enabled': enable}
)
# Also update agency settings
agency_setting, _ = AgencyAutoChatSetting.objects.get_or_create(
agency=user,
defaults={
'is_enabled': enable,
'delay_seconds': 30,
'confidence_threshold': 0.7
}
)
if enable and not agency_setting.is_enabled:
agency_setting.is_enabled = True
agency_setting.save()
return Response({
'success': True,
'property_id': str(property_id),
'auto_chat_enabled': property_state.is_auto_chat_enabled,
'custom_questions_added': len(created_questions),
'custom_questions': created_questions,
'message': f'Auto-chat {"enabled" if enable else "disabled"} with {len(created_questions)} custom questions'
}, status=status.HTTP_200_OK)
@api_view(['GET'])
@authentication_classes([JWTAuthentication])
@permission_classes([IsAuthenticated])
def get_custom_questions(request, property_id):
"""
Get custom questions for a property (for editing)
"""
user = request.user
# Verify property belongs to agency
from Property.models import Property
try:
property_obj = Property.objects.get(id=property_id, user=user)
except Property.DoesNotExist:
return Response({'error': 'Property not found'}, status=status.HTTP_404_NOT_FOUND)
custom_questions = PropertyCustomQA.objects.filter(
property_id=property_id,
is_active=True
).order_by('order')
serializer = PropertyCustomQASerializer(custom_questions, many=True)
# Get auto-chat status
auto_chat_state = PropertyAutoChatState.objects.filter(property_id=property_id).first()
return Response({
'property_id': str(property_id),
'auto_chat_enabled': auto_chat_state.is_auto_chat_enabled if auto_chat_state else False,
'custom_questions': serializer.data,
'max_allowed': 5,
'remaining_slots': 5 - len(serializer.data)
})
import uuid
from rest_framework.decorators import api_view, permission_classes
from rest_framework.permissions import IsAdminUser
@api_view(['GET', 'POST', 'PUT', 'DELETE'])
@permission_classes([IsAdminUser])
def admin_master_questions(request, question_id=None):
"""
Admin CRUD for master questions
"""
# ==================== GET ====================
if request.method == 'GET':
if question_id:
try:
question = MasterQuestion.objects.get(id=question_id)
data = {
'id': str(question.id),
'question': question.question,
'order': question.order,
'is_active': question.is_active,
'created_at': question.created_at
}
return Response(data)
except MasterQuestion.DoesNotExist:
return Response({'error': 'Question not found'}, status=404)
else:
questions = MasterQuestion.objects.all().order_by('order')
data = [{
'id': str(q.id),
'question': q.question,
'order': q.order,
'is_active': q.is_active
} for q in questions]
return Response({
'count': len(data),
'questions': data
})
# ==================== CREATE (Single) ====================
elif request.method == 'POST':
question_text = request.data.get('question')
order = request.data.get('order')
is_active = request.data.get('is_active', True)
if not question_text:
return Response({'error': 'Question text is required'}, status=400)
# Check duplicate
if MasterQuestion.objects.filter(question=question_text).exists():
return Response({'error': 'Question already exists'}, status=400)
# Auto assign order if not provided
if order is None:
order = MasterQuestion.objects.count() + 1
question = MasterQuestion.objects.create(
id=uuid.uuid4(),
question=question_text,
order=order,
is_active=is_active
)
return Response({
'id': str(question.id),
'question': question.question,
'order': question.order,
'is_active': question.is_active,
'message': 'Master question created successfully'
}, status=201)
# ==================== UPDATE ====================
elif request.method == 'PUT':
if not question_id:
return Response({'error': 'Question ID required'}, status=400)
try:
question = MasterQuestion.objects.get(id=question_id)
except MasterQuestion.DoesNotExist:
return Response({'error': 'Question not found'}, status=404)
question_text = request.data.get('question', question.question)
order = request.data.get('order', question.order)
is_active = request.data.get('is_active', question.is_active)
question.question = question_text
question.order = order
question.is_active = is_active
question.save()
return Response({
'id': str(question.id),
'question': question.question,
'order': question.order,
'is_active': question.is_active,
'message': 'Master question updated successfully'
})
# ==================== DELETE ====================
elif request.method == 'DELETE':
if not question_id:
return Response({'error': 'Question ID required'}, status=400)
try:
question = MasterQuestion.objects.get(id=question_id)
question.delete()
return Response({'message': 'Master question deleted successfully'})
except MasterQuestion.DoesNotExist:
return Response({'error': 'Question not found'}, status=404)
# ==================== NEW: BULK CREATE ====================
@api_view(['POST'])
@permission_classes([IsAdminUser])
def admin_master_questions_bulk(request):
"""
Admin can create multiple master questions at once
"""
questions_data = request.data.get('questions', [])
if not questions_data:
return Response({'error': 'Questions list is required'}, status=400)
created = []
errors = []
for idx, q_data in enumerate(questions_data):
question_text = q_data.get('question')
order = q_data.get('order', idx + 1)
is_active = q_data.get('is_active', True)
if not question_text:
errors.append({'index': idx, 'error': 'Question text required'})
continue
# Skip duplicates
if MasterQuestion.objects.filter(question=question_text).exists():
errors.append({'index': idx, 'question': question_text, 'error': 'Already exists'})
continue
try:
question = MasterQuestion.objects.create(
id=uuid.uuid4(),
question=question_text,
order=order,
is_active=is_active
)
created.append({
'id': str(question.id),
'question': question.question,
'order': question.order
})
except Exception as e:
errors.append({'index': idx, 'question': question_text, 'error': str(e)})
return Response({
'message': f'Created {len(created)} questions',
'created': created,
'errors': errors if errors else None
}, status=201 if created else 400)
# ==================== GET ACTIVE QUESTIONS FOR AGENCY ====================
@api_view(['GET'])
@authentication_classes([JWTAuthentication])
@permission_classes([IsAuthenticated])
def get_master_questions(request):
"""
Get all active master questions for agency to answer
"""
questions = MasterQuestion.objects.filter(is_active=True).order_by('order')
data = [{
'id': str(q.id),
'question': q.question,
'order': q.order
} for q in questions]
return Response({
'total': len(data),
'questions': data
})
# ==================== GLOBAL Q&A CRUD ====================
@api_view(['GET', 'POST', 'PUT', 'DELETE'])
@permission_classes([IsAdminUser])
def admin_global_qa(request, qa_id=None):
"""
Admin CRUD for Global Q&A (applies to ALL properties)
"""
# GET all or single
if request.method == 'GET':
if qa_id:
try:
qa = GlobalQA.objects.get(id=qa_id)
data = {
'id': str(qa.id),
'question': qa.question,
'answer': qa.answer,
'language': qa.language,
'priority': qa.priority,
'is_active': qa.is_active
}
return Response(data)
except GlobalQA.DoesNotExist:
return Response({'error': 'Not found'}, status=404)
else:
qa_list = GlobalQA.objects.all().order_by('-priority', 'question')
data = [{
'id': str(q.id),
'question': q.question,
'answer': q.answer[:100],
'language': q.language,
'priority': q.priority,
'is_active': q.is_active
} for q in qa_list]
return Response({'total': len(data), 'global_qa': data})
# CREATE single
elif request.method == 'POST':
question = request.data.get('question')
answer = request.data.get('answer')
language = request.data.get('language', 'both')
priority = request.data.get('priority', 0)
is_active = request.data.get('is_active', True)
if not question or not answer:
return Response({'error': 'Question and answer required'}, status=400)
if GlobalQA.objects.filter(question=question).exists():
return Response({'error': 'Question already exists'}, status=400)
# Generate embedding
embedding = embedding_service.generate_embedding(question)
qa = GlobalQA.objects.create(
id=uuid.uuid4(),
question=question,
answer=answer,
question_embedding=embedding,
language=language,
priority=priority,
is_active=is_active
)
return Response({
'id': str(qa.id),
'question': qa.question,
'answer': qa.answer,
'message': 'Global Q&A created successfully'
}, status=201)
# UPDATE
elif request.method == 'PUT':
if not qa_id:
return Response({'error': 'ID required'}, status=400)
try:
qa = GlobalQA.objects.get(id=qa_id)
except GlobalQA.DoesNotExist:
return Response({'error': 'Not found'}, status=404)
question = request.data.get('question', qa.question)
answer = request.data.get('answer', qa.answer)
language = request.data.get('language', qa.language)
priority = request.data.get('priority', qa.priority)
is_active = request.data.get('is_active', qa.is_active)
# Update embedding if question changed
if question != qa.question:
embedding = embedding_service.generate_embedding(question)
qa.question_embedding = embedding
qa.question = question
qa.answer = answer
qa.language = language
qa.priority = priority
qa.is_active = is_active
qa.save()
return Response({'message': 'Global Q&A updated successfully'})
# DELETE
elif request.method == 'DELETE':
if not qa_id:
return Response({'error': 'ID required'}, status=400)
try:
qa = GlobalQA.objects.get(id=qa_id)
qa.delete()
return Response({'message': 'Global Q&A deleted successfully'})
except GlobalQA.DoesNotExist:
return Response({'error': 'Not found'}, status=404)
# ==================== BULK CREATE GLOBAL Q&A ====================
@api_view(['POST'])
@permission_classes([IsAdminUser])
def admin_global_qa_bulk(request):
"""
Admin can create multiple Global Q&A at once
"""
items = request.data.get('items', [])
if not items:
return Response({'error': 'Items list required'}, status=400)
created = []
errors = []
for idx, item in enumerate(items):
question = item.get('question')
answer = item.get('answer')
language = item.get('language', 'both')
priority = item.get('priority', 0)
if not question or not answer:
errors.append({'index': idx, 'error': 'Question and answer required'})
continue
if GlobalQA.objects.filter(question=question).exists():
errors.append({'index': idx, 'question': question, 'error': 'Already exists'})
continue
try:
embedding = embedding_service.generate_embedding(question)
qa = GlobalQA.objects.create(
id=uuid.uuid4(),
question=question,
answer=answer,
question_embedding=embedding,
language=language,
priority=priority,
is_active=True
)
created.append({
'id': str(qa.id),
'question': qa.question,
'answer': qa.answer[:50]
})
except Exception as e:
errors.append({'index': idx, 'question': question, 'error': str(e)})
return Response({
'message': f'Created {len(created)} Global Q&A',
'created': created,
'errors': errors if errors else None
}, status=201 if created else 400)