update
This commit is contained in:
Binary file not shown.
Binary file not shown.
0
Backend/src/ai/__init__.py
Normal file
0
Backend/src/ai/__init__.py
Normal file
0
Backend/src/ai/models/__init__.py
Normal file
0
Backend/src/ai/models/__init__.py
Normal file
138
Backend/src/ai/models/ai_conversation.py
Normal file
138
Backend/src/ai/models/ai_conversation.py
Normal file
@@ -0,0 +1,138 @@
|
||||
"""
|
||||
AI Conversation History and Learning Models
|
||||
Stores conversation history and learned patterns for self-training
|
||||
"""
|
||||
|
||||
from sqlalchemy import Column, Integer, String, Text, ForeignKey, DateTime, Boolean, Numeric, JSON, Index
|
||||
from sqlalchemy.orm import relationship
|
||||
from datetime import datetime
|
||||
from ...shared.config.database import Base
|
||||
|
||||
|
||||
class AIConversation(Base):
|
||||
"""Stores AI assistant conversation history"""
|
||||
__tablename__ = 'ai_conversations'
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True, autoincrement=True)
|
||||
user_id = Column(Integer, ForeignKey('users.id'), nullable=False, index=True)
|
||||
session_id = Column(String(100), nullable=True, index=True) # For grouping related conversations
|
||||
user_query = Column(Text, nullable=False)
|
||||
ai_response = Column(Text, nullable=False)
|
||||
intent = Column(String(100), nullable=True, index=True)
|
||||
context_used = Column(JSON, nullable=True) # Store context data used
|
||||
user_role = Column(String(50), nullable=True, index=True)
|
||||
response_time_ms = Column(Integer, nullable=True) # Response time in milliseconds
|
||||
is_helpful = Column(Boolean, nullable=True) # User feedback on helpfulness
|
||||
created_at = Column(DateTime, default=datetime.utcnow, nullable=False, index=True)
|
||||
|
||||
# Relationships
|
||||
user = relationship('User', foreign_keys=[user_id])
|
||||
feedbacks = relationship('AIConversationFeedback', back_populates='conversation', cascade='all, delete-orphan')
|
||||
learned_patterns = relationship('AILearnedPattern', back_populates='source_conversation')
|
||||
|
||||
__table_args__ = (
|
||||
Index('idx_ai_conv_user_date', 'user_id', 'created_at'),
|
||||
Index('idx_ai_conv_intent', 'intent'),
|
||||
Index('idx_ai_conv_session', 'session_id'),
|
||||
)
|
||||
|
||||
|
||||
class AIConversationFeedback(Base):
|
||||
"""User feedback on AI responses for learning"""
|
||||
__tablename__ = 'ai_conversation_feedbacks'
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True, autoincrement=True)
|
||||
conversation_id = Column(Integer, ForeignKey('ai_conversations.id'), nullable=False, index=True)
|
||||
user_id = Column(Integer, ForeignKey('users.id'), nullable=False)
|
||||
rating = Column(Integer, nullable=True) # 1-5 rating
|
||||
is_helpful = Column(Boolean, nullable=True)
|
||||
is_correct = Column(Boolean, nullable=True)
|
||||
feedback_text = Column(Text, nullable=True) # User's text feedback
|
||||
correction = Column(Text, nullable=True) # If user provides corrected answer
|
||||
created_at = Column(DateTime, default=datetime.utcnow, nullable=False)
|
||||
|
||||
# Relationships
|
||||
conversation = relationship('AIConversation', back_populates='feedbacks')
|
||||
user = relationship('User', foreign_keys=[user_id])
|
||||
|
||||
|
||||
class AILearnedPattern(Base):
|
||||
"""Learned patterns from conversations for pattern matching"""
|
||||
__tablename__ = 'ai_learned_patterns'
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True, autoincrement=True)
|
||||
pattern_keywords = Column(Text, nullable=False) # Keywords extracted from query
|
||||
query_pattern = Column(Text, nullable=False) # Pattern template
|
||||
intent = Column(String(100), nullable=False, index=True)
|
||||
response_template = Column(Text, nullable=True) # Response template
|
||||
context_keys = Column(JSON, nullable=True) # Which context keys to use
|
||||
confidence_score = Column(Numeric(5, 2), nullable=False, default=0.0) # Confidence 0-100
|
||||
usage_count = Column(Integer, nullable=False, default=0) # How many times used
|
||||
success_count = Column(Integer, nullable=False, default=0) # How many successful matches
|
||||
source_conversation_id = Column(Integer, ForeignKey('ai_conversations.id'), nullable=True)
|
||||
user_role = Column(String(50), nullable=True, index=True) # Role-specific patterns
|
||||
is_active = Column(Boolean, nullable=False, default=True)
|
||||
created_at = Column(DateTime, default=datetime.utcnow, nullable=False)
|
||||
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False)
|
||||
last_used_at = Column(DateTime, nullable=True)
|
||||
|
||||
# Relationships
|
||||
source_conversation = relationship('AIConversation', back_populates='learned_patterns')
|
||||
|
||||
__table_args__ = (
|
||||
Index('idx_ai_pattern_intent_role', 'intent', 'user_role'),
|
||||
Index('idx_ai_pattern_active', 'is_active', 'confidence_score'),
|
||||
)
|
||||
|
||||
|
||||
class AIKnowledgeEntry(Base):
|
||||
"""Dynamic knowledge base entries learned from interactions"""
|
||||
__tablename__ = 'ai_knowledge_entries'
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True, autoincrement=True)
|
||||
topic = Column(String(200), nullable=False, index=True)
|
||||
question = Column(Text, nullable=False) # Common question format
|
||||
answer = Column(Text, nullable=False) # Best answer
|
||||
keywords = Column(JSON, nullable=True) # Keywords for matching
|
||||
related_intent = Column(String(100), nullable=True, index=True)
|
||||
source = Column(String(100), nullable=True) # 'user_feedback', 'admin_added', 'learned'
|
||||
confidence = Column(Numeric(5, 2), nullable=False, default=50.0) # 0-100
|
||||
usage_count = Column(Integer, nullable=False, default=0)
|
||||
success_count = Column(Integer, nullable=False, default=0)
|
||||
user_role = Column(String(50), nullable=True, index=True) # Role-specific knowledge
|
||||
is_verified = Column(Boolean, nullable=False, default=False) # Verified by admin
|
||||
created_by_user_id = Column(Integer, ForeignKey('users.id'), nullable=True)
|
||||
created_at = Column(DateTime, default=datetime.utcnow, nullable=False)
|
||||
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False)
|
||||
last_used_at = Column(DateTime, nullable=True)
|
||||
|
||||
# Relationships
|
||||
created_by = relationship('User', foreign_keys=[created_by_user_id])
|
||||
|
||||
__table_args__ = (
|
||||
Index('idx_ai_knowledge_topic_role', 'topic', 'user_role'),
|
||||
Index('idx_ai_knowledge_active', 'is_verified', 'confidence'),
|
||||
)
|
||||
|
||||
|
||||
class AITrainingMetrics(Base):
|
||||
"""Metrics for tracking AI training and improvement"""
|
||||
__tablename__ = 'ai_training_metrics'
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True, autoincrement=True)
|
||||
metric_date = Column(DateTime, nullable=False, index=True) # Date of metrics
|
||||
total_conversations = Column(Integer, nullable=False, default=0)
|
||||
total_patterns_learned = Column(Integer, nullable=False, default=0)
|
||||
total_knowledge_entries = Column(Integer, nullable=False, default=0)
|
||||
average_response_time_ms = Column(Integer, nullable=True)
|
||||
average_rating = Column(Numeric(3, 2), nullable=True) # Average user rating
|
||||
helpful_rate = Column(Numeric(5, 2), nullable=True) # Percentage of helpful responses
|
||||
correct_rate = Column(Numeric(5, 2), nullable=True) # Percentage of correct responses
|
||||
pattern_match_success_rate = Column(Numeric(5, 2), nullable=True)
|
||||
knowledge_usage_rate = Column(Numeric(5, 2), nullable=True)
|
||||
created_at = Column(DateTime, default=datetime.utcnow, nullable=False)
|
||||
|
||||
__table_args__ = (
|
||||
Index('idx_ai_metrics_date', 'metric_date'),
|
||||
)
|
||||
|
||||
@@ -2,7 +2,7 @@ from sqlalchemy import Column, Integer, String, Text, ForeignKey, DateTime, Enum
|
||||
from sqlalchemy.orm import relationship
|
||||
from datetime import datetime
|
||||
import enum
|
||||
from ..config.database import Base
|
||||
from ...shared.config.database import Base
|
||||
|
||||
class ChatStatus(str, enum.Enum):
|
||||
pending = 'pending'
|
||||
0
Backend/src/ai/routes/__init__.py
Normal file
0
Backend/src/ai/routes/__init__.py
Normal file
295
Backend/src/ai/routes/ai_assistant_routes.py
Normal file
295
Backend/src/ai/routes/ai_assistant_routes.py
Normal file
@@ -0,0 +1,295 @@
|
||||
"""
|
||||
AI Assistant API Routes
|
||||
"""
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlalchemy.orm import Session
|
||||
from typing import Optional
|
||||
from pydantic import BaseModel
|
||||
|
||||
from ...shared.config.database import get_db
|
||||
from ...security.middleware.auth import get_current_user
|
||||
from ...auth.models.user import User
|
||||
from ...auth.models.role import Role
|
||||
from ..services.ai_chat_service import AIChatService
|
||||
from ..services.ai_assistant_service import AIAssistantService
|
||||
from ..services.ai_learning_service import AILearningService
|
||||
from ..services.ai_training_scheduler import get_training_scheduler
|
||||
from ...shared.config.logging_config import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
router = APIRouter(prefix="/ai-assistant", tags=["AI Assistant"])
|
||||
|
||||
|
||||
class ChatMessageRequest(BaseModel):
|
||||
message: str
|
||||
context: Optional[dict] = None
|
||||
|
||||
|
||||
class ChatMessageResponse(BaseModel):
|
||||
response: str
|
||||
intent: str
|
||||
data_used: dict
|
||||
timestamp: str
|
||||
|
||||
|
||||
@router.post("/chat")
|
||||
async def chat_with_ai(
|
||||
request: ChatMessageRequest,
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
Chat with AI assistant
|
||||
Accessible to all authenticated users (admin, staff, accountant, customer)
|
||||
Responses are filtered based on user role
|
||||
"""
|
||||
try:
|
||||
# Load user role
|
||||
user_role = db.query(Role).filter(Role.id == current_user.role_id).first()
|
||||
if not user_role:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="User role not found"
|
||||
)
|
||||
|
||||
# Initialize AI chat service with current user for role-based responses
|
||||
ai_service = AIChatService(db, current_user)
|
||||
|
||||
# Generate response with role awareness
|
||||
result = ai_service.generate_response(
|
||||
user_query=request.message,
|
||||
context=request.context
|
||||
)
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"data": {
|
||||
"response": result["response"],
|
||||
"intent": result.get("intent", "unknown"),
|
||||
"data_used": result.get("data_used", {}),
|
||||
"timestamp": result.get("timestamp", ""),
|
||||
"user_role": user_role.name
|
||||
}
|
||||
}
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Error in AI chat: {str(e)}", exc_info=True)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Error processing AI request: {str(e)}"
|
||||
)
|
||||
|
||||
|
||||
@router.get("/status")
|
||||
async def get_ai_status(
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
Get comprehensive system status for AI assistant
|
||||
Status is filtered based on user role
|
||||
"""
|
||||
try:
|
||||
# Load user role
|
||||
user_role = db.query(Role).filter(Role.id == current_user.role_id).first()
|
||||
if not user_role:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="User role not found"
|
||||
)
|
||||
|
||||
# Initialize AI assistant with current user for role-based context
|
||||
ai_assistant = AIAssistantService(db, current_user)
|
||||
context = ai_assistant.generate_context_for_ai()
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"data": context
|
||||
}
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting AI status: {str(e)}", exc_info=True)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=str(e)
|
||||
)
|
||||
|
||||
|
||||
@router.get("/rooms/occupied")
|
||||
async def get_occupied_rooms(
|
||||
room_number: Optional[str] = None,
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Get list of occupied rooms - Admin and Staff only"""
|
||||
try:
|
||||
user_role = db.query(Role).filter(Role.id == current_user.role_id).first()
|
||||
if not user_role or user_role.name not in ['staff', 'admin']:
|
||||
raise HTTPException(status_code=403, detail="Only staff and admin can access this information")
|
||||
|
||||
ai_assistant = AIAssistantService(db, current_user)
|
||||
rooms = ai_assistant.search_occupied_rooms(room_number)
|
||||
|
||||
return {"status": "success", "data": {"rooms": rooms}}
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting occupied rooms: {str(e)}", exc_info=True)
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.get("/rooms/problems")
|
||||
async def get_room_problems(
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Get rooms with problems - Admin and Staff only"""
|
||||
try:
|
||||
user_role = db.query(Role).filter(Role.id == current_user.role_id).first()
|
||||
if not user_role or user_role.name not in ['staff', 'admin']:
|
||||
raise HTTPException(status_code=403, detail="Only staff and admin can access this information")
|
||||
|
||||
ai_assistant = AIAssistantService(db, current_user)
|
||||
problems = ai_assistant.get_room_problems()
|
||||
|
||||
return {"status": "success", "data": {"problems": problems}}
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting room problems: {str(e)}", exc_info=True)
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.get("/chats/unanswered")
|
||||
async def get_unanswered_chats(
|
||||
hours: int = 24,
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Get unanswered chat messages - Admin and Staff only"""
|
||||
try:
|
||||
user_role = db.query(Role).filter(Role.id == current_user.role_id).first()
|
||||
if not user_role or user_role.name not in ['staff', 'admin']:
|
||||
raise HTTPException(status_code=403, detail="Only staff and admin can access this information")
|
||||
|
||||
ai_assistant = AIAssistantService(db, current_user)
|
||||
chats = ai_assistant.get_unanswered_chats(hours)
|
||||
|
||||
return {"status": "success", "data": {"chats": chats}}
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting unanswered chats: {str(e)}", exc_info=True)
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.post("/training/trigger")
|
||||
async def trigger_manual_training(
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Manually trigger AI training - Admin only"""
|
||||
try:
|
||||
user_role = db.query(Role).filter(Role.id == current_user.role_id).first()
|
||||
if not user_role or user_role.name != 'admin':
|
||||
raise HTTPException(status_code=403, detail="Only admin can trigger manual training")
|
||||
|
||||
learning_service = AILearningService(db)
|
||||
result = learning_service.auto_train_from_all_conversations()
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"message": "Training completed",
|
||||
"data": result
|
||||
}
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Error triggering training: {str(e)}", exc_info=True)
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.post("/training/analyze")
|
||||
async def trigger_manual_analysis(
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Manually trigger AI analysis and improvement - Admin only"""
|
||||
try:
|
||||
user_role = db.query(Role).filter(Role.id == current_user.role_id).first()
|
||||
if not user_role or user_role.name != 'admin':
|
||||
raise HTTPException(status_code=403, detail="Only admin can trigger manual analysis")
|
||||
|
||||
learning_service = AILearningService(db)
|
||||
result = learning_service.auto_analyze_and_improve()
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"message": "Analysis completed",
|
||||
"data": result
|
||||
}
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Error triggering analysis: {str(e)}", exc_info=True)
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.get("/training/status")
|
||||
async def get_training_status(
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Get AI training status and metrics - Admin only"""
|
||||
try:
|
||||
user_role = db.query(Role).filter(Role.id == current_user.role_id).first()
|
||||
if not user_role or user_role.name != 'admin':
|
||||
raise HTTPException(status_code=403, detail="Only admin can view training status")
|
||||
|
||||
from ..models.ai_conversation import AIConversation, AILearnedPattern, AIKnowledgeEntry, AITrainingMetrics
|
||||
from sqlalchemy import func
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
# Get statistics
|
||||
total_conversations = db.query(AIConversation).count()
|
||||
total_patterns = db.query(AILearnedPattern).filter(AILearnedPattern.is_active == True).count()
|
||||
total_knowledge = db.query(AIKnowledgeEntry).count()
|
||||
|
||||
# Get recent metrics
|
||||
recent_metrics = db.query(AITrainingMetrics).order_by(
|
||||
AITrainingMetrics.metric_date.desc()
|
||||
).limit(1).first()
|
||||
|
||||
# Get scheduler status
|
||||
scheduler = get_training_scheduler()
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"data": {
|
||||
"scheduler_running": scheduler.running,
|
||||
"last_training": scheduler.last_training_time.isoformat() if scheduler.last_training_time else None,
|
||||
"last_analysis": scheduler.last_analysis_time.isoformat() if scheduler.last_analysis_time else None,
|
||||
"statistics": {
|
||||
"total_conversations": total_conversations,
|
||||
"total_patterns": total_patterns,
|
||||
"total_knowledge_entries": total_knowledge,
|
||||
},
|
||||
"recent_metrics": {
|
||||
"average_rating": float(recent_metrics.average_rating) if recent_metrics and recent_metrics.average_rating else None,
|
||||
"helpful_rate": float(recent_metrics.helpful_rate) if recent_metrics and recent_metrics.helpful_rate else None,
|
||||
"average_response_time_ms": recent_metrics.average_response_time_ms if recent_metrics else None,
|
||||
} if recent_metrics else None
|
||||
}
|
||||
}
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting training status: {str(e)}", exc_info=True)
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
0
Backend/src/ai/schemas/__init__.py
Normal file
0
Backend/src/ai/schemas/__init__.py
Normal file
0
Backend/src/ai/services/__init__.py
Normal file
0
Backend/src/ai/services/__init__.py
Normal file
434
Backend/src/ai/services/ai_assistant_service.py
Normal file
434
Backend/src/ai/services/ai_assistant_service.py
Normal file
@@ -0,0 +1,434 @@
|
||||
"""
|
||||
Enterprise AI Assistant Service
|
||||
Monitors rooms, bookings, invoices, payments, and chat messages
|
||||
Provides intelligent responses to queries about hotel operations
|
||||
"""
|
||||
|
||||
from typing import Dict, List, Optional, Any
|
||||
from datetime import datetime, timedelta
|
||||
from sqlalchemy.orm import Session, joinedload
|
||||
from sqlalchemy import and_, or_, func
|
||||
import json
|
||||
import logging
|
||||
|
||||
from ...rooms.models.room import Room, RoomStatus
|
||||
from ...bookings.models.booking import Booking, BookingStatus
|
||||
from ...payments.models.invoice import Invoice
|
||||
from ...payments.models.payment import Payment, PaymentStatus
|
||||
from ..models.chat import Chat, ChatMessage, ChatStatus
|
||||
from ...auth.models.user import User
|
||||
from ...shared.config.logging_config import get_logger
|
||||
from .ai_knowledge_base import AIKnowledgeBase
|
||||
from .ai_role_access_service import AIRoleAccessService
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class AIAssistantService:
|
||||
"""Enterprise AI Assistant for Hotel Management"""
|
||||
|
||||
def __init__(self, db: Session, current_user: Optional[User] = None):
|
||||
self.db = db
|
||||
self.current_user = current_user
|
||||
self.role_access_service = AIRoleAccessService(db, current_user) if current_user else None
|
||||
|
||||
def get_room_status_summary(self) -> Dict[str, Any]:
|
||||
"""Get comprehensive room status summary"""
|
||||
try:
|
||||
total_rooms = self.db.query(Room).count()
|
||||
available_rooms = self.db.query(Room).filter(Room.status == RoomStatus.available).count()
|
||||
occupied_rooms = self.db.query(Room).filter(Room.status == RoomStatus.occupied).count()
|
||||
maintenance_rooms = self.db.query(Room).filter(Room.status == RoomStatus.maintenance).count()
|
||||
cleaning_rooms = self.db.query(Room).filter(Room.status == RoomStatus.cleaning).count()
|
||||
|
||||
# Get rooms with bookings today
|
||||
today = datetime.utcnow().date()
|
||||
rooms_with_bookings = self.db.query(Booking).filter(
|
||||
and_(
|
||||
Booking.status != BookingStatus.cancelled,
|
||||
Booking.check_in_date <= today,
|
||||
Booking.check_out_date > today
|
||||
)
|
||||
).count()
|
||||
|
||||
return {
|
||||
"total_rooms": total_rooms,
|
||||
"available": available_rooms,
|
||||
"occupied": occupied_rooms,
|
||||
"maintenance": maintenance_rooms,
|
||||
"cleaning": cleaning_rooms,
|
||||
"rooms_with_active_bookings": rooms_with_bookings,
|
||||
"occupancy_rate": round((occupied_rooms / total_rooms * 100) if total_rooms > 0 else 0, 2)
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting room status: {str(e)}", exc_info=True)
|
||||
return {}
|
||||
|
||||
def get_booking_summary(self, days: int = 7) -> Dict[str, Any]:
|
||||
"""Get booking summary for specified days"""
|
||||
try:
|
||||
today = datetime.utcnow().date()
|
||||
end_date = today + timedelta(days=days)
|
||||
|
||||
# Upcoming check-ins
|
||||
upcoming_checkins = self.db.query(Booking).filter(
|
||||
and_(
|
||||
Booking.status == BookingStatus.confirmed,
|
||||
Booking.check_in_date >= today,
|
||||
Booking.check_in_date <= end_date
|
||||
)
|
||||
).count()
|
||||
|
||||
# Upcoming check-outs
|
||||
upcoming_checkouts = self.db.query(Booking).filter(
|
||||
and_(
|
||||
Booking.status == BookingStatus.confirmed,
|
||||
Booking.check_out_date >= today,
|
||||
Booking.check_out_date <= end_date
|
||||
)
|
||||
).count()
|
||||
|
||||
# Active bookings
|
||||
active_bookings = self.db.query(Booking).filter(
|
||||
and_(
|
||||
Booking.status == BookingStatus.confirmed,
|
||||
Booking.check_in_date <= today,
|
||||
Booking.check_out_date > today
|
||||
)
|
||||
).count()
|
||||
|
||||
# Pending bookings
|
||||
pending_bookings = self.db.query(Booking).filter(
|
||||
Booking.status == BookingStatus.pending
|
||||
).count()
|
||||
|
||||
return {
|
||||
"upcoming_checkins": upcoming_checkins,
|
||||
"upcoming_checkouts": upcoming_checkouts,
|
||||
"active_bookings": active_bookings,
|
||||
"pending_bookings": pending_bookings,
|
||||
"period_days": days
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting booking summary: {str(e)}", exc_info=True)
|
||||
return {}
|
||||
|
||||
def get_payment_summary(self, days: int = 30) -> Dict[str, Any]:
|
||||
"""Get payment summary"""
|
||||
try:
|
||||
today = datetime.utcnow().date()
|
||||
start_date = today - timedelta(days=days)
|
||||
|
||||
# Total payments in period
|
||||
total_payments = self.db.query(func.sum(Payment.amount)).filter(
|
||||
and_(
|
||||
Payment.payment_status == PaymentStatus.completed,
|
||||
func.date(Payment.payment_date) >= start_date,
|
||||
func.date(Payment.payment_date) <= today
|
||||
)
|
||||
).scalar() or 0
|
||||
|
||||
# Pending payments
|
||||
pending_payments = self.db.query(func.sum(Payment.amount)).filter(
|
||||
Payment.payment_status == PaymentStatus.pending
|
||||
).scalar() or 0
|
||||
|
||||
# Payment count
|
||||
completed_count = self.db.query(Payment).filter(
|
||||
and_(
|
||||
Payment.payment_status == PaymentStatus.completed,
|
||||
func.date(Payment.payment_date) >= start_date
|
||||
)
|
||||
).count()
|
||||
|
||||
return {
|
||||
"total_revenue": float(total_payments),
|
||||
"pending_amount": float(pending_payments),
|
||||
"completed_payments": completed_count,
|
||||
"period_days": days
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting payment summary: {str(e)}", exc_info=True)
|
||||
return {}
|
||||
|
||||
def get_invoice_summary(self) -> Dict[str, Any]:
|
||||
"""Get invoice summary"""
|
||||
try:
|
||||
total_invoices = self.db.query(Invoice).count()
|
||||
|
||||
# Group by status
|
||||
status_counts = {}
|
||||
invoices = self.db.query(Invoice).all()
|
||||
for invoice in invoices:
|
||||
status = invoice.status if hasattr(invoice, 'status') else 'unknown'
|
||||
status_counts[status] = status_counts.get(status, 0) + 1
|
||||
|
||||
# Overdue invoices
|
||||
today = datetime.utcnow().date()
|
||||
overdue_count = self.db.query(Invoice).filter(
|
||||
and_(
|
||||
Invoice.due_date < today,
|
||||
Invoice.status != 'paid'
|
||||
)
|
||||
).count() if hasattr(Invoice, 'due_date') else 0
|
||||
|
||||
return {
|
||||
"total_invoices": total_invoices,
|
||||
"status_breakdown": status_counts,
|
||||
"overdue_invoices": overdue_count
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting invoice summary: {str(e)}", exc_info=True)
|
||||
return {}
|
||||
|
||||
def get_unanswered_chats(self, hours: int = 24) -> List[Dict[str, Any]]:
|
||||
"""Get unanswered chat messages from after working hours"""
|
||||
try:
|
||||
cutoff_time = datetime.utcnow() - timedelta(hours=hours)
|
||||
|
||||
# Get open chats with unread messages
|
||||
chats = self.db.query(Chat).filter(
|
||||
and_(
|
||||
Chat.status == ChatStatus.open,
|
||||
Chat.updated_at >= cutoff_time
|
||||
)
|
||||
).all()
|
||||
|
||||
unanswered = []
|
||||
for chat in chats:
|
||||
# Get last message
|
||||
last_message = self.db.query(ChatMessage).filter(
|
||||
ChatMessage.chat_id == chat.id
|
||||
).order_by(ChatMessage.created_at.desc()).first()
|
||||
|
||||
if last_message and last_message.sender_type == 'visitor' and not last_message.is_read:
|
||||
unanswered.append({
|
||||
"chat_id": chat.id,
|
||||
"visitor_name": chat.visitor_name,
|
||||
"visitor_email": chat.visitor_email,
|
||||
"last_message": last_message.message[:100],
|
||||
"last_message_time": last_message.created_at.isoformat(),
|
||||
"waiting_hours": (datetime.utcnow() - last_message.created_at).total_seconds() / 3600
|
||||
})
|
||||
|
||||
return unanswered
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting unanswered chats: {str(e)}", exc_info=True)
|
||||
return []
|
||||
|
||||
def search_occupied_rooms(self, room_number: Optional[str] = None) -> List[Dict[str, Any]]:
|
||||
"""Search for occupied rooms"""
|
||||
try:
|
||||
today = datetime.utcnow().date()
|
||||
|
||||
# Get rooms with active bookings
|
||||
query = self.db.query(Room).options(
|
||||
joinedload(Room.room_type)
|
||||
).join(Booking).filter(
|
||||
and_(
|
||||
Room.status == RoomStatus.occupied,
|
||||
Booking.status == BookingStatus.confirmed,
|
||||
Booking.check_in_date <= today,
|
||||
Booking.check_out_date > today
|
||||
)
|
||||
)
|
||||
|
||||
if room_number:
|
||||
query = query.filter(Room.room_number.ilike(f"%{room_number}%"))
|
||||
|
||||
rooms = query.all()
|
||||
|
||||
result = []
|
||||
for room in rooms:
|
||||
# Get the active booking for this room
|
||||
booking = self.db.query(Booking).options(
|
||||
joinedload(Booking.user)
|
||||
).filter(
|
||||
and_(
|
||||
Booking.room_id == room.id,
|
||||
Booking.status == BookingStatus.confirmed,
|
||||
Booking.check_in_date <= today,
|
||||
Booking.check_out_date > today
|
||||
)
|
||||
).first()
|
||||
|
||||
result.append({
|
||||
"room_number": room.room_number,
|
||||
"room_type": room.room_type.name if room.room_type else "Unknown",
|
||||
"floor": room.floor,
|
||||
"booking_id": booking.id if booking else None,
|
||||
"guest_name": booking.user.full_name if booking and booking.user else "Unknown",
|
||||
"check_in": booking.check_in_date.isoformat() if booking else None,
|
||||
"check_out": booking.check_out_date.isoformat() if booking else None
|
||||
})
|
||||
|
||||
return result
|
||||
except Exception as e:
|
||||
logger.error(f"Error searching occupied rooms: {str(e)}", exc_info=True)
|
||||
return []
|
||||
|
||||
def get_room_problems(self) -> List[Dict[str, Any]]:
|
||||
"""Get rooms with problems (maintenance, issues)"""
|
||||
try:
|
||||
# Rooms in maintenance
|
||||
maintenance_rooms = self.db.query(Room).filter(
|
||||
Room.status == RoomStatus.maintenance
|
||||
).all()
|
||||
|
||||
problems = []
|
||||
for room in maintenance_rooms:
|
||||
problems.append({
|
||||
"room_number": room.room_number,
|
||||
"issue_type": "maintenance",
|
||||
"status": room.status.value,
|
||||
"description": f"Room {room.room_number} is under maintenance"
|
||||
})
|
||||
|
||||
# Check for maintenance records
|
||||
try:
|
||||
from ...rooms.models.room_maintenance import RoomMaintenance, MaintenanceStatus
|
||||
active_maintenance = self.db.query(RoomMaintenance).filter(
|
||||
MaintenanceStatus.in_progress == MaintenanceStatus.in_progress
|
||||
).all()
|
||||
|
||||
for maint in active_maintenance:
|
||||
room = self.db.query(Room).filter(Room.id == maint.room_id).first()
|
||||
if room:
|
||||
problems.append({
|
||||
"room_number": room.room_number,
|
||||
"issue_type": "maintenance",
|
||||
"status": maint.status.value if hasattr(maint.status, 'value') else str(maint.status),
|
||||
"description": maint.description or f"Maintenance issue in room {room.room_number}",
|
||||
"scheduled_start": maint.scheduled_start.isoformat() if maint.scheduled_start else None
|
||||
})
|
||||
except ImportError:
|
||||
pass # Maintenance model might not exist
|
||||
|
||||
return problems
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting room problems: {str(e)}", exc_info=True)
|
||||
return []
|
||||
|
||||
def get_booking_by_number(self, booking_number: str) -> Optional[Dict[str, Any]]:
|
||||
"""Get booking details by booking number"""
|
||||
try:
|
||||
booking = self.db.query(Booking).filter(
|
||||
Booking.booking_number == booking_number
|
||||
).first()
|
||||
|
||||
if not booking:
|
||||
return None
|
||||
|
||||
return {
|
||||
"booking_number": booking.booking_number,
|
||||
"status": booking.status.value if hasattr(booking.status, 'value') else str(booking.status),
|
||||
"guest_name": booking.user.full_name if booking.user else "Unknown",
|
||||
"room_number": booking.room.room_number if booking.room else "Unknown",
|
||||
"check_in": booking.check_in_date.isoformat(),
|
||||
"check_out": booking.check_out_date.isoformat(),
|
||||
"total_price": float(booking.total_price) if booking.total_price else 0,
|
||||
"payment_status": self._get_booking_payment_status(booking.id)
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting booking: {str(e)}", exc_info=True)
|
||||
return None
|
||||
|
||||
def get_invoice_by_number(self, invoice_number: str) -> Optional[Dict[str, Any]]:
|
||||
"""Get invoice details by invoice number"""
|
||||
try:
|
||||
invoice = self.db.query(Invoice).filter(
|
||||
Invoice.invoice_number == invoice_number
|
||||
).first()
|
||||
|
||||
if not invoice:
|
||||
return None
|
||||
|
||||
return {
|
||||
"invoice_number": invoice.invoice_number,
|
||||
"status": invoice.status if hasattr(invoice, 'status') else "unknown",
|
||||
"amount": float(invoice.total_amount) if hasattr(invoice, 'total_amount') else 0,
|
||||
"due_date": invoice.due_date.isoformat() if hasattr(invoice, 'due_date') and invoice.due_date else None,
|
||||
"booking_number": invoice.booking.booking_number if invoice.booking else None,
|
||||
"created_at": invoice.created_at.isoformat() if hasattr(invoice, 'created_at') else None
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting invoice: {str(e)}", exc_info=True)
|
||||
return None
|
||||
|
||||
def _get_booking_payment_status(self, booking_id: int) -> str:
|
||||
"""Get payment status for a booking"""
|
||||
try:
|
||||
payments = self.db.query(Payment).filter(
|
||||
Payment.booking_id == booking_id
|
||||
).all()
|
||||
|
||||
if not payments:
|
||||
return "unpaid"
|
||||
|
||||
completed = [p for p in payments if p.payment_status == PaymentStatus.completed]
|
||||
if completed:
|
||||
total_paid = sum(float(p.amount) for p in completed)
|
||||
booking = self.db.query(Booking).filter(Booking.id == booking_id).first()
|
||||
if booking and booking.total_price:
|
||||
if total_paid >= float(booking.total_price):
|
||||
return "paid"
|
||||
else:
|
||||
return "partially_paid"
|
||||
|
||||
return "pending"
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting payment status: {str(e)}", exc_info=True)
|
||||
return "unknown"
|
||||
|
||||
def generate_context_for_ai(self) -> Dict[str, Any]:
|
||||
"""Generate comprehensive context for AI assistant with role-based filtering"""
|
||||
context = {
|
||||
"timestamp": datetime.utcnow().isoformat(),
|
||||
}
|
||||
|
||||
# Use role-based access service if user is provided
|
||||
if self.role_access_service:
|
||||
role_name = self.role_access_service.role_name
|
||||
context["user_role"] = role_name
|
||||
context["role_capabilities"] = AIKnowledgeBase.get_role_info(role_name)
|
||||
|
||||
# Get role-appropriate data
|
||||
context["room_status"] = self.role_access_service.get_room_status_summary()
|
||||
|
||||
# Get user-specific bookings
|
||||
context["user_bookings"] = self.role_access_service.get_user_bookings(limit=5)
|
||||
|
||||
# Financial data only for authorized roles
|
||||
if self.role_access_service.can_access_feature("payment_summary"):
|
||||
payment_summary = self.role_access_service.get_payment_summary()
|
||||
if payment_summary:
|
||||
context["payment_summary"] = payment_summary
|
||||
|
||||
invoice_summary = self.role_access_service.get_invoice_summary()
|
||||
if invoice_summary:
|
||||
context["invoice_summary"] = invoice_summary
|
||||
|
||||
# Operational data for staff and admin
|
||||
if role_name in ["admin", "staff"]:
|
||||
context["booking_summary"] = self.get_booking_summary()
|
||||
context["unanswered_chats"] = self.get_unanswered_chats()
|
||||
context["room_problems"] = self.get_room_problems()
|
||||
|
||||
# Add knowledge base information
|
||||
context["application_knowledge"] = {
|
||||
"features": list(AIKnowledgeBase.FEATURES.keys()),
|
||||
"role_info": AIKnowledgeBase.get_role_info(role_name)
|
||||
}
|
||||
else:
|
||||
# Default context without user (shouldn't happen in production)
|
||||
context.update({
|
||||
"room_status": self.get_room_status_summary(),
|
||||
"booking_summary": self.get_booking_summary(),
|
||||
"payment_summary": self.get_payment_summary(),
|
||||
"invoice_summary": self.get_invoice_summary(),
|
||||
"unanswered_chats": self.get_unanswered_chats(),
|
||||
"room_problems": self.get_room_problems(),
|
||||
})
|
||||
|
||||
return context
|
||||
|
||||
975
Backend/src/ai/services/ai_chat_service.py
Normal file
975
Backend/src/ai/services/ai_chat_service.py
Normal file
@@ -0,0 +1,975 @@
|
||||
"""
|
||||
AI Chat Service - Handles AI-powered chat responses
|
||||
Integrates with OpenAI, Anthropic, or other LLM providers
|
||||
"""
|
||||
|
||||
from typing import Dict, List, Optional, Any
|
||||
from datetime import datetime
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
|
||||
from ..services.ai_assistant_service import AIAssistantService
|
||||
from ..services.ai_knowledge_base import AIKnowledgeBase
|
||||
from ..services.ai_role_access_service import AIRoleAccessService
|
||||
from ..services.ai_learning_service import AILearningService
|
||||
from ...auth.models.user import User
|
||||
from ..models.ai_conversation import AILearnedPattern
|
||||
from ...shared.config.logging_config import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class AIChatService:
|
||||
"""AI Chat Service for generating intelligent responses"""
|
||||
|
||||
def __init__(self, db_session, current_user: Optional[User] = None):
|
||||
self.db = db_session
|
||||
self.current_user = current_user
|
||||
self.ai_assistant = AIAssistantService(db_session, current_user)
|
||||
self.knowledge_base = AIKnowledgeBase()
|
||||
self.role_access_service = AIRoleAccessService(db_session, current_user) if current_user else None
|
||||
self.learning_service = AILearningService(db_session)
|
||||
# Configure AI provider (OpenAI, Anthropic, etc.)
|
||||
self.provider = os.getenv("AI_PROVIDER", "openai")
|
||||
self.api_key = os.getenv("AI_API_KEY", "")
|
||||
self.model = os.getenv("AI_MODEL", "gpt-4")
|
||||
|
||||
def generate_response(self, user_query: str, context: Optional[Dict] = None, session_id: Optional[str] = None) -> Dict[str, Any]:
|
||||
"""
|
||||
Generate AI response based on user query with self-learning
|
||||
"""
|
||||
start_time = datetime.utcnow()
|
||||
|
||||
try:
|
||||
# Check learned patterns first
|
||||
role_name = self.role_access_service.role_name if self.role_access_service else None
|
||||
learned_pattern = self.learning_service.get_learned_pattern(user_query, role_name)
|
||||
knowledge_entry = self.learning_service.get_knowledge_entry(user_query, role_name)
|
||||
|
||||
# Check for similar past queries
|
||||
similar_queries = self.learning_service.find_similar_queries(user_query, limit=3)
|
||||
|
||||
# Get current system context
|
||||
system_context = self.ai_assistant.generate_context_for_ai()
|
||||
|
||||
# Merge with provided context
|
||||
if context:
|
||||
system_context.update(context)
|
||||
|
||||
# Add learned knowledge to context
|
||||
if learned_pattern:
|
||||
system_context["learned_pattern"] = {
|
||||
"pattern": learned_pattern.query_pattern,
|
||||
"intent": learned_pattern.intent,
|
||||
"confidence": float(learned_pattern.confidence_score)
|
||||
}
|
||||
|
||||
if knowledge_entry:
|
||||
system_context["knowledge_entry"] = knowledge_entry
|
||||
|
||||
if similar_queries:
|
||||
system_context["similar_past_queries"] = similar_queries
|
||||
|
||||
# Analyze query intent (or use learned intent)
|
||||
intent = learned_pattern.intent if learned_pattern else self._analyze_intent(user_query)
|
||||
|
||||
# Get relevant data based on intent
|
||||
relevant_data = self._get_relevant_data(intent, user_query)
|
||||
|
||||
# Status-related intents should always use rule-based formatted responses
|
||||
# These provide consistent, well-formatted output with emojis and structure
|
||||
status_intents = [
|
||||
"general_status", "room_status", "room_occupied", "room_available",
|
||||
"room_problems", "booking_status", "payment_status", "invoice_status",
|
||||
"chat_status", "app_info"
|
||||
]
|
||||
|
||||
response = None
|
||||
|
||||
# For status queries, prioritize rule-based formatted responses
|
||||
if intent in status_intents:
|
||||
# Always use rule-based response for status queries to ensure proper formatting
|
||||
if self.api_key and self.provider == "openai":
|
||||
# Even with LLM, prefer rule-based for status to maintain format consistency
|
||||
response = self._generate_rule_based_response(intent, user_query, relevant_data)
|
||||
else:
|
||||
response = self._generate_rule_based_response(intent, user_query, relevant_data)
|
||||
logger.info(f"Using rule-based formatted response for status intent: {intent}")
|
||||
else:
|
||||
# For non-status queries, use learned patterns/knowledge if available
|
||||
if learned_pattern and learned_pattern.confidence_score >= 70.0 and learned_pattern.response_template:
|
||||
# Try to use learned pattern response
|
||||
try:
|
||||
response = self._apply_learned_pattern(learned_pattern, relevant_data, user_query)
|
||||
if response:
|
||||
logger.info(f"Using learned pattern {learned_pattern.id} for query")
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to apply learned pattern: {str(e)}")
|
||||
|
||||
# Use knowledge entry if available
|
||||
if not response and knowledge_entry and knowledge_entry.get("confidence", 0) >= 70.0:
|
||||
response = knowledge_entry.get("answer")
|
||||
logger.info(f"Using knowledge entry for query")
|
||||
|
||||
# Generate response using LLM or rule-based system if no learned response
|
||||
if not response:
|
||||
if self.api_key and self.provider == "openai":
|
||||
response = self._generate_llm_response(user_query, system_context, relevant_data)
|
||||
else:
|
||||
# Fallback to rule-based responses
|
||||
response = self._generate_rule_based_response(intent, user_query, relevant_data)
|
||||
|
||||
# Calculate response time
|
||||
response_time = int((datetime.utcnow() - start_time).total_seconds() * 1000)
|
||||
|
||||
# Save conversation for learning (async, don't block response)
|
||||
if self.current_user:
|
||||
try:
|
||||
self.learning_service.save_conversation(
|
||||
user_id=self.current_user.id,
|
||||
user_query=user_query,
|
||||
ai_response=response,
|
||||
intent=intent,
|
||||
context_used=relevant_data,
|
||||
user_role=role_name,
|
||||
session_id=session_id,
|
||||
response_time_ms=response_time
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to save conversation for learning: {str(e)}")
|
||||
|
||||
return {
|
||||
"response": response,
|
||||
"intent": intent,
|
||||
"data_used": relevant_data,
|
||||
"timestamp": datetime.utcnow().isoformat(),
|
||||
"used_learned_pattern": learned_pattern.id if learned_pattern else None,
|
||||
"used_knowledge_entry": knowledge_entry.get("topic") if knowledge_entry else None
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"Error generating AI response: {str(e)}", exc_info=True)
|
||||
return {
|
||||
"response": "I apologize, but I'm having trouble processing your request right now. Please try again later.",
|
||||
"error": str(e)
|
||||
}
|
||||
|
||||
def _analyze_intent(self, query: str) -> str:
|
||||
"""Analyze user query intent with comprehensive pattern matching"""
|
||||
query_lower = query.lower()
|
||||
|
||||
# Room-related queries - expanded patterns
|
||||
room_keywords = ['room', 'rooms', 'occupied', 'available', 'free', 'vacant', 'vacancy',
|
||||
'occupancy', 'suite', 'suites', 'accommodation']
|
||||
if any(word in query_lower for word in room_keywords):
|
||||
if any(word in query_lower for word in ['occupied', 'booked', 'taken', 'in use', 'guest']):
|
||||
return "room_occupied"
|
||||
elif any(word in query_lower for word in ['available', 'free', 'vacant', 'empty', 'ready']):
|
||||
return "room_available"
|
||||
elif any(word in query_lower for word in ['problem', 'issue', 'maintenance', 'broken', 'repair', 'fix']):
|
||||
return "room_problems"
|
||||
elif any(word in query_lower for word in ['how many', 'total', 'count', 'number of', 'how much', 'quantity']):
|
||||
return "room_status"
|
||||
elif any(word in query_lower for word in ['status', 'state', 'condition', 'overview']):
|
||||
return "room_status"
|
||||
return "room_status"
|
||||
|
||||
# Booking-related queries - expanded patterns
|
||||
booking_keywords = ['booking', 'bookings', 'reservation', 'reservations', 'check-in', 'check-out',
|
||||
'checkin', 'checkout', 'arrival', 'departure', 'guest', 'guests']
|
||||
if any(word in query_lower for word in booking_keywords):
|
||||
if any(word in query_lower for word in ['number', 'id', 'code', 'reference']):
|
||||
return "booking_by_number"
|
||||
elif any(word in query_lower for word in ['upcoming', 'coming', 'next', 'future', 'scheduled']):
|
||||
return "booking_status"
|
||||
elif any(word in query_lower for word in ['active', 'current', 'today', 'now']):
|
||||
return "booking_status"
|
||||
return "booking_status"
|
||||
|
||||
# Invoice-related queries - expanded patterns
|
||||
invoice_keywords = ['invoice', 'invoices', 'bill', 'bills', 'receipt', 'receipts', 'statement', 'statements']
|
||||
if any(word in query_lower for word in invoice_keywords):
|
||||
if any(word in query_lower for word in ['number', 'id', 'code', 'reference']):
|
||||
return "invoice_by_number"
|
||||
elif any(word in query_lower for word in ['overdue', 'unpaid', 'pending', 'due']):
|
||||
return "invoice_status"
|
||||
return "invoice_status"
|
||||
|
||||
# Payment-related queries - expanded patterns
|
||||
payment_keywords = ['payment', 'payments', 'paid', 'unpaid', 'revenue', 'money', 'income',
|
||||
'earnings', 'sales', 'financial', 'cash', 'transaction', 'transactions']
|
||||
if any(word in query_lower for word in payment_keywords):
|
||||
if any(word in query_lower for word in ['all', 'list', 'show all', 'every', 'entire']):
|
||||
return "all_payments"
|
||||
return "payment_status"
|
||||
|
||||
# Chat-related queries - expanded patterns
|
||||
chat_keywords = ['chat', 'chats', 'message', 'messages', 'unanswered', 'customer', 'customers',
|
||||
'inquiry', 'inquiries', 'support', 'help', 'contact']
|
||||
if any(word in query_lower for word in chat_keywords):
|
||||
return "chat_status"
|
||||
|
||||
# App/Platform information queries - for admin only
|
||||
app_keywords = ['app', 'application', 'platform', 'system', 'software', 'what is this',
|
||||
'about the app', 'about the platform', 'about the system', 'features',
|
||||
'capabilities', 'what can', 'how does the app', 'tell me about the app',
|
||||
'what does this app do', 'app features', 'system features', 'platform features',
|
||||
'what features', 'list features', 'show features', 'tell me about features',
|
||||
'what modules', 'what models', 'system overview', 'platform overview']
|
||||
if any(word in query_lower for word in app_keywords):
|
||||
return "app_info"
|
||||
|
||||
# General status queries - expanded patterns
|
||||
status_keywords = ['status', 'summary', 'overview', 'dashboard', 'report', 'reports',
|
||||
'statistics', 'stats', 'info', 'information', 'details', 'what', 'show me', 'tell me']
|
||||
if any(word in query_lower for word in status_keywords):
|
||||
return "general_status"
|
||||
|
||||
# Questions about "how many", "what", "show", "tell" - default to general status
|
||||
question_words = ['how many', 'how much', 'what', 'show', 'tell', 'give me', 'list', 'display']
|
||||
if any(word in query_lower for word in question_words):
|
||||
return "general_status"
|
||||
|
||||
return "general_query"
|
||||
|
||||
def _get_relevant_data(self, intent: str, query: str) -> Dict[str, Any]:
|
||||
"""Get relevant data based on intent with role-based filtering"""
|
||||
data = {}
|
||||
role_name = self.role_access_service.role_name if self.role_access_service else "customer"
|
||||
|
||||
try:
|
||||
if intent == "room_occupied":
|
||||
# Only staff/admin can see occupied room details
|
||||
if role_name in ["admin", "staff"]:
|
||||
room_number = self._extract_room_number(query)
|
||||
data["occupied_rooms"] = self.ai_assistant.search_occupied_rooms(room_number)
|
||||
data["room_summary"] = self.ai_assistant.get_room_status_summary()
|
||||
|
||||
elif intent == "room_available":
|
||||
data["room_summary"] = self.ai_assistant.get_room_status_summary()
|
||||
|
||||
elif intent == "room_status":
|
||||
data["room_summary"] = self.ai_assistant.get_room_status_summary()
|
||||
|
||||
elif intent == "room_problems":
|
||||
# Only staff/admin can see room problems
|
||||
if role_name in ["admin", "staff"]:
|
||||
data["problems"] = self.ai_assistant.get_room_problems()
|
||||
|
||||
elif intent == "booking_by_number":
|
||||
booking_number = self._extract_booking_number(query)
|
||||
if booking_number:
|
||||
booking_data = self.ai_assistant.get_booking_by_number(booking_number)
|
||||
if booking_data:
|
||||
# Filter based on role
|
||||
if role_name == "customer":
|
||||
# Customers can only see their own bookings
|
||||
user_bookings = self.role_access_service.get_user_bookings(limit=100)
|
||||
if any(b.get("booking_number") == booking_number for b in user_bookings):
|
||||
data["booking"] = booking_data
|
||||
else:
|
||||
data["booking"] = booking_data
|
||||
|
||||
elif intent == "booking_status":
|
||||
if role_name in ["admin", "staff"]:
|
||||
data["booking_summary"] = self.ai_assistant.get_booking_summary()
|
||||
elif role_name == "customer":
|
||||
# Customers see their own bookings
|
||||
data["user_bookings"] = self.role_access_service.get_user_bookings(limit=10)
|
||||
|
||||
elif intent == "invoice_by_number":
|
||||
invoice_number = self._extract_invoice_number(query)
|
||||
if invoice_number:
|
||||
invoice_data = self.ai_assistant.get_invoice_by_number(invoice_number)
|
||||
if invoice_data:
|
||||
# Filter based on role
|
||||
if role_name == "customer":
|
||||
user_invoices = self.role_access_service.get_user_invoices(limit=100)
|
||||
if any(i.get("invoice_number") == invoice_number for i in user_invoices):
|
||||
data["invoice"] = invoice_data
|
||||
else:
|
||||
data["invoice"] = invoice_data
|
||||
|
||||
elif intent == "invoice_status":
|
||||
if role_name in ["admin", "accountant", "staff"]:
|
||||
invoice_summary = self.role_access_service.get_invoice_summary()
|
||||
if invoice_summary:
|
||||
data["invoice_summary"] = invoice_summary
|
||||
elif role_name == "customer":
|
||||
# Customers see their own invoices
|
||||
data["user_invoices"] = self.role_access_service.get_user_invoices(limit=10)
|
||||
|
||||
elif intent == "all_payments":
|
||||
# Get all payments for admin/accountant
|
||||
if role_name in ["admin", "accountant"]:
|
||||
data["all_payments"] = self.role_access_service.get_all_payments()
|
||||
elif role_name == "staff":
|
||||
data["all_payments"] = self.role_access_service.get_user_payments(limit=50)
|
||||
elif role_name == "customer":
|
||||
data["all_payments"] = self.role_access_service.get_user_payments(limit=None)
|
||||
|
||||
elif intent == "payment_status":
|
||||
if role_name in ["admin", "accountant", "staff"]:
|
||||
payment_summary = self.role_access_service.get_payment_summary()
|
||||
if payment_summary:
|
||||
data["payment_summary"] = payment_summary
|
||||
elif role_name == "customer":
|
||||
# Customers see their own payments
|
||||
data["user_payments"] = self.role_access_service.get_user_payments(limit=10)
|
||||
|
||||
elif intent == "chat_status":
|
||||
# Only staff/admin can see unanswered chats
|
||||
if role_name in ["admin", "staff"]:
|
||||
data["unanswered_chats"] = self.ai_assistant.get_unanswered_chats()
|
||||
|
||||
elif intent == "general_status":
|
||||
data["room_summary"] = self.ai_assistant.get_room_status_summary()
|
||||
|
||||
if role_name in ["admin", "staff"]:
|
||||
data["booking_summary"] = self.ai_assistant.get_booking_summary()
|
||||
data["unanswered_chats"] = self.ai_assistant.get_unanswered_chats()
|
||||
data["room_problems"] = self.ai_assistant.get_room_problems()
|
||||
elif role_name == "customer":
|
||||
data["user_bookings"] = self.role_access_service.get_user_bookings(limit=5)
|
||||
data["user_invoices"] = self.role_access_service.get_user_invoices(limit=5)
|
||||
data["user_payments"] = self.role_access_service.get_user_payments(limit=5)
|
||||
|
||||
# Financial summaries only for authorized roles
|
||||
if role_name in ["admin", "accountant", "staff"]:
|
||||
payment_summary = self.role_access_service.get_payment_summary()
|
||||
if payment_summary:
|
||||
data["payment_summary"] = payment_summary
|
||||
|
||||
invoice_summary = self.role_access_service.get_invoice_summary()
|
||||
if invoice_summary:
|
||||
data["invoice_summary"] = invoice_summary
|
||||
|
||||
elif intent == "app_info":
|
||||
# App information - only for admin
|
||||
if role_name == "admin":
|
||||
# Get comprehensive application knowledge
|
||||
app_knowledge = self.knowledge_base.get_application_knowledge()
|
||||
data["app_knowledge"] = app_knowledge
|
||||
else:
|
||||
# Non-admin users get limited information
|
||||
data["app_knowledge"] = {
|
||||
"overview": self.knowledge_base.APPLICATION_OVERVIEW,
|
||||
"role_info": self.knowledge_base.get_role_info(role_name)
|
||||
}
|
||||
|
||||
elif intent == "general_query":
|
||||
# For general queries, get role-appropriate data
|
||||
data["room_summary"] = self.ai_assistant.get_room_status_summary()
|
||||
|
||||
if role_name in ["admin", "staff"]:
|
||||
data["booking_summary"] = self.ai_assistant.get_booking_summary()
|
||||
elif role_name == "customer":
|
||||
data["user_bookings"] = self.role_access_service.get_user_bookings(limit=5)
|
||||
|
||||
if role_name in ["admin", "accountant"]:
|
||||
payment_summary = self.role_access_service.get_payment_summary()
|
||||
if payment_summary:
|
||||
data["payment_summary"] = payment_summary
|
||||
|
||||
invoice_summary = self.role_access_service.get_invoice_summary()
|
||||
if invoice_summary:
|
||||
data["invoice_summary"] = invoice_summary
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting relevant data: {str(e)}", exc_info=True)
|
||||
|
||||
return data
|
||||
|
||||
def _extract_room_number(self, query: str) -> Optional[str]:
|
||||
"""Extract room number from query"""
|
||||
import re
|
||||
# Look for patterns like "room 101", "room101", "#101"
|
||||
patterns = [
|
||||
r'room\s*#?\s*(\d+)',
|
||||
r'#\s*(\d+)',
|
||||
r'room\s+(\d{3,4})',
|
||||
]
|
||||
for pattern in patterns:
|
||||
match = re.search(pattern, query, re.IGNORECASE)
|
||||
if match:
|
||||
return match.group(1)
|
||||
return None
|
||||
|
||||
def _extract_booking_number(self, query: str) -> Optional[str]:
|
||||
"""Extract booking number from query"""
|
||||
import re
|
||||
# Look for booking number patterns
|
||||
patterns = [
|
||||
r'booking\s*#?\s*([A-Z0-9-]+)',
|
||||
r'BK[A-Z0-9-]+',
|
||||
]
|
||||
for pattern in patterns:
|
||||
match = re.search(pattern, query, re.IGNORECASE)
|
||||
if match:
|
||||
return match.group(1) if match.groups() else match.group(0)
|
||||
return None
|
||||
|
||||
def _extract_invoice_number(self, query: str) -> Optional[str]:
|
||||
"""Extract invoice number from query"""
|
||||
import re
|
||||
patterns = [
|
||||
r'invoice\s*#?\s*([A-Z0-9-]+)',
|
||||
r'INV[A-Z0-9-]+',
|
||||
]
|
||||
for pattern in patterns:
|
||||
match = re.search(pattern, query, re.IGNORECASE)
|
||||
if match:
|
||||
return match.group(1) if match.groups() else match.group(0)
|
||||
return None
|
||||
|
||||
def _generate_llm_response(self, query: str, context: Dict, data: Dict) -> str:
|
||||
"""Generate response using LLM (OpenAI, Anthropic, etc.)"""
|
||||
try:
|
||||
if self.provider == "openai":
|
||||
import openai
|
||||
openai.api_key = self.api_key
|
||||
|
||||
# Build system prompt
|
||||
system_prompt = self._build_system_prompt(context, data)
|
||||
|
||||
# Call OpenAI API
|
||||
response = openai.ChatCompletion.create(
|
||||
model=self.model,
|
||||
messages=[
|
||||
{"role": "system", "content": system_prompt},
|
||||
{"role": "user", "content": query}
|
||||
],
|
||||
temperature=0.7,
|
||||
max_tokens=500
|
||||
)
|
||||
|
||||
return response.choices[0].message.content.strip()
|
||||
|
||||
except ImportError:
|
||||
logger.warning("OpenAI library not installed, falling back to rule-based responses")
|
||||
return self._generate_rule_based_response(self._analyze_intent(query), query, data)
|
||||
except Exception as e:
|
||||
logger.error(f"Error calling LLM: {str(e)}", exc_info=True)
|
||||
return self._generate_rule_based_response(self._analyze_intent(query), query, data)
|
||||
|
||||
def _build_system_prompt(self, context: Dict, data: Dict) -> str:
|
||||
"""Build enterprise-level system prompt for LLM with role awareness"""
|
||||
role_name = context.get("user_role", "customer")
|
||||
role_info = self.knowledge_base.get_role_info(role_name)
|
||||
|
||||
prompt = f"""You are an enterprise AI assistant for a comprehensive Hotel Booking and Management System (HBMS).
|
||||
You provide intelligent, role-aware assistance based on the user's permissions and access level.
|
||||
|
||||
USER ROLE: {role_name.upper()}
|
||||
ROLE DESCRIPTION: {role_info.get('description', 'Standard user access')}
|
||||
|
||||
YOUR CAPABILITIES:
|
||||
"""
|
||||
|
||||
# Add role-specific capabilities
|
||||
can_access = role_info.get("can_access", [])
|
||||
for capability in can_access[:15]: # Limit to avoid token overflow
|
||||
prompt += f"- {capability}\n"
|
||||
|
||||
if role_info.get("cannot_access"):
|
||||
prompt += "\nRESTRICTIONS:\n"
|
||||
for restriction in role_info.get("cannot_access", [])[:5]:
|
||||
prompt += f"- Cannot access: {restriction}\n"
|
||||
|
||||
prompt += """
|
||||
\nSYSTEM FEATURES AVAILABLE:
|
||||
- Booking Management (create, view, modify, cancel bookings)
|
||||
- Room Management (availability, status, types, amenities)
|
||||
- Payment Processing (multiple payment methods, status tracking)
|
||||
- Invoice Management (generation, tracking, payment reconciliation)
|
||||
- Loyalty Program (points, tiers, rewards, referrals)
|
||||
- Service Management (additional hotel services)
|
||||
- Guest Management (profiles, preferences, CRM)
|
||||
- Chat Support System
|
||||
- Check-in/Check-out Operations
|
||||
- Maintenance and Housekeeping
|
||||
|
||||
IMPORTANT SECURITY RULES:
|
||||
1. NEVER provide information about other users' bookings, payments, or personal data unless user is Admin
|
||||
2. For customers: Only show information about their own bookings, invoices, and payments
|
||||
3. For staff: Provide operational data but not financial summaries
|
||||
4. For accountant: Provide financial data but not operational details
|
||||
5. For admin: Full access to all information
|
||||
|
||||
Current System Status:
|
||||
"""
|
||||
prompt += json.dumps(context, indent=2, default=str)
|
||||
|
||||
if data:
|
||||
prompt += "\n\nRelevant Data for Current Query:\n"
|
||||
prompt += json.dumps(data, indent=2, default=str)
|
||||
|
||||
prompt += """
|
||||
\nINSTRUCTIONS:
|
||||
- Provide clear, concise, and helpful responses
|
||||
- Use the data provided to answer questions accurately
|
||||
- Respect role-based access restrictions
|
||||
- If you don't have specific information or the user doesn't have access, explain clearly
|
||||
- Be professional and friendly
|
||||
- For customers, always refer to data as "your bookings", "your invoices", etc.
|
||||
- For staff/admin, provide comprehensive operational insights
|
||||
- For accountants, focus on financial data and reports"""
|
||||
|
||||
return prompt
|
||||
|
||||
def _generate_rule_based_response(self, intent: str, query: str, data: Dict) -> str:
|
||||
"""Generate rule-based response when LLM is not available - with role awareness"""
|
||||
role_name = self.role_access_service.role_name if self.role_access_service else "customer"
|
||||
is_customer = role_name == "customer"
|
||||
|
||||
if intent == "room_occupied":
|
||||
if data.get("occupied_rooms"):
|
||||
rooms = data["occupied_rooms"]
|
||||
if len(rooms) == 1:
|
||||
room = rooms[0]
|
||||
return f"Room {room['room_number']} is currently occupied by {room['guest_name']}. Check-in: {room['check_in']}, Check-out: {room['check_out']}."
|
||||
else:
|
||||
return f"There are {len(rooms)} occupied rooms. Here are the details: {', '.join([r['room_number'] for r in rooms[:5]])}"
|
||||
else:
|
||||
summary = data.get("room_summary", {})
|
||||
return f"Currently, {summary.get('occupied', 0)} rooms are occupied out of {summary.get('total_rooms', 0)} total rooms."
|
||||
|
||||
elif intent == "room_available":
|
||||
summary = data.get("room_summary", {})
|
||||
return f"There are {summary.get('available', 0)} available rooms out of {summary.get('total_rooms', 0)} total rooms. {summary.get('cleaning', 0)} rooms are being cleaned."
|
||||
|
||||
elif intent == "room_status":
|
||||
summary = data.get("room_summary", {})
|
||||
total = summary.get('total_rooms', 0)
|
||||
available = summary.get('available', 0)
|
||||
occupied = summary.get('occupied', 0)
|
||||
maintenance = summary.get('maintenance', 0)
|
||||
cleaning = summary.get('cleaning', 0)
|
||||
occupancy_rate = summary.get('occupancy_rate', 0)
|
||||
|
||||
response = f"We have {total} total rooms. "
|
||||
response += f"Currently: {available} available, {occupied} occupied, {cleaning} being cleaned, and {maintenance} under maintenance. "
|
||||
response += f"Occupancy rate: {occupancy_rate}%."
|
||||
return response
|
||||
|
||||
elif intent == "room_problems":
|
||||
problems = data.get("problems", [])
|
||||
if problems:
|
||||
response = f"⚠️ Room Issues: There are {len(problems)} rooms with problems:\n\n"
|
||||
for i, problem in enumerate(problems[:10], 1):
|
||||
response += f"{i}. Room {problem['room_number']}\n"
|
||||
response += f" Issue: {problem.get('issue_type', 'Unknown')}\n"
|
||||
if problem.get('description'):
|
||||
response += f" Details: {problem['description']}\n"
|
||||
if problem.get('status'):
|
||||
response += f" Status: {problem['status']}\n"
|
||||
response += "\n"
|
||||
if len(problems) > 10:
|
||||
response += f"... and {len(problems) - 10} more rooms with issues."
|
||||
return response
|
||||
else:
|
||||
return "✅ No room problems reported at this time. All rooms are in good condition!"
|
||||
|
||||
elif intent == "booking_by_number":
|
||||
booking = data.get("booking")
|
||||
if booking:
|
||||
response = f"Booking {booking['booking_number']}:\n"
|
||||
response += f"• Status: {booking['status']}\n"
|
||||
response += f"• Guest: {booking['guest_name']}\n"
|
||||
response += f"• Room: {booking['room_number']}\n"
|
||||
response += f"• Check-in: {booking['check_in']}\n"
|
||||
response += f"• Check-out: {booking['check_out']}\n"
|
||||
response += f"• Payment Status: {booking['payment_status']}\n"
|
||||
if booking.get('total_price'):
|
||||
response += f"• Total Price: ${float(booking['total_price']):,.2f}"
|
||||
return response
|
||||
else:
|
||||
return "I couldn't find that booking. Please check the booking number and try again."
|
||||
|
||||
elif intent == "booking_status":
|
||||
# Role-aware booking status
|
||||
if is_customer:
|
||||
user_bookings = data.get("user_bookings", [])
|
||||
if user_bookings:
|
||||
response = f"You have {len(user_bookings)} booking(s):\n\n"
|
||||
for booking in user_bookings[:5]:
|
||||
response += f"• {booking.get('booking_number', 'N/A')}: {booking.get('status', 'Unknown')}\n"
|
||||
response += f" Check-in: {booking.get('check_in', 'N/A')}, Check-out: {booking.get('check_out', 'N/A')}\n"
|
||||
if len(user_bookings) > 5:
|
||||
response += f"\n... and {len(user_bookings) - 5} more bookings."
|
||||
return response
|
||||
else:
|
||||
return "You don't have any bookings at this time."
|
||||
else:
|
||||
summary = data.get("booking_summary", {})
|
||||
if summary:
|
||||
response = "Booking Status Summary:\n"
|
||||
response += f"• Active Bookings: {summary.get('active_bookings', 0)}\n"
|
||||
response += f"• Upcoming Check-ins: {summary.get('upcoming_checkins', 0)}\n"
|
||||
response += f"• Upcoming Check-outs: {summary.get('upcoming_checkouts', 0)}\n"
|
||||
response += f"• Pending Bookings: {summary.get('pending_bookings', 0)}"
|
||||
return response
|
||||
return "No booking data available at this time."
|
||||
|
||||
elif intent == "invoice_by_number":
|
||||
invoice = data.get("invoice")
|
||||
if invoice:
|
||||
response = f"Invoice {invoice['invoice_number']}:\n"
|
||||
response += f"• Status: {invoice['status']}\n"
|
||||
response += f"• Amount: ${invoice['amount']:,.2f}\n"
|
||||
if invoice.get('due_date'):
|
||||
response += f"• Due Date: {invoice['due_date']}\n"
|
||||
if invoice.get('booking_number'):
|
||||
response += f"• Booking: {invoice['booking_number']}"
|
||||
return response
|
||||
else:
|
||||
return "I couldn't find that invoice. Please check the invoice number."
|
||||
|
||||
elif intent == "invoice_status":
|
||||
# Role-aware invoice status
|
||||
if is_customer:
|
||||
user_invoices = data.get("user_invoices", [])
|
||||
if user_invoices:
|
||||
response = f"You have {len(user_invoices)} invoice(s):\n\n"
|
||||
for invoice in user_invoices[:5]:
|
||||
status_emoji = "✅" if invoice.get('status') == 'paid' else "⏳" if invoice.get('status') == 'sent' else "⚠️"
|
||||
response += f"{status_emoji} Invoice {invoice.get('invoice_number', 'N/A')}: {invoice.get('status', 'Unknown')}\n"
|
||||
response += f" Amount: ${invoice.get('total_amount', 0):,.2f}, Due: {invoice.get('due_date', 'N/A')}\n"
|
||||
if len(user_invoices) > 5:
|
||||
response += f"\n... and {len(user_invoices) - 5} more invoices."
|
||||
return response
|
||||
else:
|
||||
return "You don't have any invoices at this time."
|
||||
else:
|
||||
summary = data.get("invoice_summary", {})
|
||||
if summary:
|
||||
response = "Invoice Status Summary:\n"
|
||||
response += f"• Total Invoices: {summary.get('total_invoices', 0)}\n"
|
||||
response += f"• Overdue Invoices: {summary.get('overdue_invoices', 0)}"
|
||||
if summary.get('status_breakdown'):
|
||||
response += f"\n• Status Breakdown: {summary['status_breakdown']}"
|
||||
return response
|
||||
return "No invoice data available at this time."
|
||||
|
||||
elif intent == "all_payments":
|
||||
# Display all payments in table format
|
||||
all_payments = data.get("all_payments", [])
|
||||
if all_payments:
|
||||
response = f"📋 ALL PAYMENTS ({len(all_payments)} total):\n\n"
|
||||
response += "┌──────────────┬──────────────────────────────┬──────────────────┬──────────┬──────────────────┬──────────┬────────────┬──────────────┐\n"
|
||||
response += "│ Transaction │ Booking Number │ Customer │ Method │ Type │ Status │ Amount │ Payment Date │\n"
|
||||
response += "├──────────────┼──────────────────────────────┼──────────────────┼──────────┼──────────────────┼──────────┼────────────┼──────────────┤\n"
|
||||
|
||||
for payment in all_payments:
|
||||
trans_id = (payment.get('transaction_id') or f"PAY-{payment.get('id', 'N/A')}")[:12]
|
||||
booking = (payment.get('booking_number') or 'N/A')[:28]
|
||||
customer = (payment.get('customer_name') or 'N/A')[:16]
|
||||
method = str(payment.get('payment_method', 'N/A')).replace('_', ' ').title()[:8]
|
||||
ptype = payment.get('payment_type_display', payment.get('payment_type', 'N/A'))[:16]
|
||||
status = str(payment.get('payment_status', 'N/A')).title()
|
||||
|
||||
status_emoji = "✅" if status.lower() == 'completed' else "⏳" if status.lower() == 'pending' else "❌" if status.lower() == 'failed' else "🔄" if status.lower() == 'refunded' else "❓"
|
||||
status_display = f"{status_emoji} {status}"[:10]
|
||||
|
||||
amount = payment.get('amount', 0)
|
||||
amount_str = f"{amount:,.2f} €"[:12]
|
||||
|
||||
pdate = payment.get('payment_date', None)
|
||||
if pdate and pdate != 'Invalid Date':
|
||||
try:
|
||||
from datetime import datetime
|
||||
if isinstance(pdate, str):
|
||||
if 'T' in pdate:
|
||||
dt = datetime.fromisoformat(pdate.replace('Z', '+00:00'))
|
||||
pdate = dt.strftime('%Y-%m-%d')
|
||||
else:
|
||||
pdate = pdate[:10]
|
||||
else:
|
||||
pdate = pdate.strftime('%Y-%m-%d') if hasattr(pdate, 'strftime') else str(pdate)[:10]
|
||||
except Exception:
|
||||
pdate = "Invalid Date"
|
||||
else:
|
||||
pdate = "Invalid Date"
|
||||
|
||||
response += f"│ {trans_id:<12} │ {booking:<28} │ {customer:<16} │ {method:<8} │ {ptype:<16} │ {status_display:<10} │ {amount_str:<12} │ {pdate:<14} │\n"
|
||||
|
||||
response += "└──────────────┴──────────────────────────────┴──────────────────┴──────────┴──────────────────┴──────────┴────────────┴──────────────┘\n"
|
||||
|
||||
# Add summary statistics
|
||||
completed = sum(1 for p in all_payments if p.get('payment_status', '').lower() == 'completed')
|
||||
pending = sum(1 for p in all_payments if p.get('payment_status', '').lower() == 'pending')
|
||||
failed = sum(1 for p in all_payments if p.get('payment_status', '').lower() == 'failed')
|
||||
refunded = sum(1 for p in all_payments if p.get('payment_status', '').lower() == 'refunded')
|
||||
total_amount = sum(float(p.get('amount', 0)) for p in all_payments)
|
||||
completed_amount = sum(float(p.get('amount', 0)) for p in all_payments if p.get('payment_status', '').lower() == 'completed')
|
||||
pending_amount = sum(float(p.get('amount', 0)) for p in all_payments if p.get('payment_status', '').lower() == 'pending')
|
||||
|
||||
response += f"\n📊 Summary:\n"
|
||||
response += f"• Total Payments: {len(all_payments)}\n"
|
||||
response += f"• ✅ Completed: {completed} ({completed_amount:,.2f} €)\n"
|
||||
response += f"• ⏳ Pending: {pending} ({pending_amount:,.2f} €)\n"
|
||||
response += f"• ❌ Failed: {failed}\n"
|
||||
if refunded > 0:
|
||||
response += f"• 🔄 Refunded: {refunded}\n"
|
||||
response += f"• Total Amount: {total_amount:,.2f} €"
|
||||
|
||||
return response
|
||||
else:
|
||||
return "No payments found."
|
||||
|
||||
elif intent == "payment_status":
|
||||
# Role-aware payment status
|
||||
if is_customer:
|
||||
user_payments = data.get("user_payments", [])
|
||||
if user_payments:
|
||||
response = f"Your recent payments ({len(user_payments)} total):\n\n"
|
||||
for payment in user_payments[:5]:
|
||||
status_emoji = "✅" if payment.get('payment_status') == 'completed' else "⏳" if payment.get('payment_status') == 'pending' else "❌"
|
||||
response += f"{status_emoji} ${payment.get('amount', 0):,.2f} - {payment.get('payment_method', 'N/A')}\n"
|
||||
response += f" Status: {payment.get('payment_status', 'Unknown')}, Date: {payment.get('payment_date', 'N/A')}\n"
|
||||
if len(user_payments) > 5:
|
||||
response += f"\n... and {len(user_payments) - 5} more payments."
|
||||
return response
|
||||
else:
|
||||
return "You don't have any payment records at this time."
|
||||
else:
|
||||
summary = data.get("payment_summary", {})
|
||||
if summary:
|
||||
response = "Payment Status Summary:\n"
|
||||
response += f"• Total Revenue (last 30 days): ${summary.get('total_revenue', 0):,.2f}\n"
|
||||
response += f"• Pending Payments: ${summary.get('pending_amount', 0):,.2f}\n"
|
||||
response += f"• Completed Payments: {summary.get('completed_payments', 0)}"
|
||||
return response
|
||||
return "No payment data available at this time."
|
||||
|
||||
elif intent == "chat_status":
|
||||
chats = data.get("unanswered_chats", [])
|
||||
if chats:
|
||||
response = f"Chat Status: There are {len(chats)} unanswered chat messages.\n\n"
|
||||
response += "Oldest waiting messages:\n"
|
||||
for i, chat in enumerate(chats[:3], 1):
|
||||
response += f"{i}. {chat['visitor_name']} - Waiting {chat['waiting_hours']:.1f} hours\n"
|
||||
response += f" Last message: {chat['last_message'][:50]}...\n"
|
||||
return response
|
||||
else:
|
||||
return "All chat messages have been answered. Great job! ✅"
|
||||
|
||||
elif intent == "general_status":
|
||||
# Comprehensive status overview - always show all sections for consistency
|
||||
room_summary = data.get("room_summary", {})
|
||||
booking_summary = data.get("booking_summary", {})
|
||||
payment_summary = data.get("payment_summary", {})
|
||||
invoice_summary = data.get("invoice_summary", {})
|
||||
unanswered_chats = data.get("unanswered_chats", [])
|
||||
problems = data.get("room_problems", [])
|
||||
|
||||
response = "📊 Hotel Status Overview:\n\n"
|
||||
|
||||
# Rooms - always show
|
||||
response += "🏨 ROOMS:\n"
|
||||
occupancy_rate = room_summary.get('occupancy_rate', 0)
|
||||
# Format occupancy rate to show one decimal place
|
||||
if isinstance(occupancy_rate, (int, float)):
|
||||
occupancy_rate = f"{occupancy_rate:.1f}"
|
||||
response += f"• Total: {room_summary.get('total_rooms', 0)}\n"
|
||||
response += f"• Available: {room_summary.get('available', 0)}\n"
|
||||
response += f"• Occupied: {room_summary.get('occupied', 0)}\n"
|
||||
response += f"• Cleaning: {room_summary.get('cleaning', 0)}\n"
|
||||
response += f"• Maintenance: {room_summary.get('maintenance', 0)}\n"
|
||||
response += f"• Occupancy Rate: {occupancy_rate}%\n\n"
|
||||
|
||||
# Bookings - always show
|
||||
response += "📅 BOOKINGS:\n"
|
||||
response += f"• Active: {booking_summary.get('active_bookings', 0)}\n"
|
||||
response += f"• Upcoming Check-ins: {booking_summary.get('upcoming_checkins', 0)}\n"
|
||||
response += f"• Upcoming Check-outs: {booking_summary.get('upcoming_checkouts', 0)}\n"
|
||||
response += f"• Pending: {booking_summary.get('pending_bookings', 0)}\n\n"
|
||||
|
||||
# Payments - always show
|
||||
response += "💰 PAYMENTS:\n"
|
||||
total_revenue = payment_summary.get('total_revenue', 0) or 0
|
||||
pending_amount = payment_summary.get('pending_amount', 0) or 0
|
||||
response += f"• Revenue (30 days): ${total_revenue:,.2f}\n"
|
||||
response += f"• Pending: ${pending_amount:,.2f}\n"
|
||||
response += f"• Completed: {payment_summary.get('completed_payments', 0)}\n\n"
|
||||
|
||||
# Invoices - always show
|
||||
response += "🧾 INVOICES:\n"
|
||||
response += f"• Total: {invoice_summary.get('total_invoices', 0)}\n"
|
||||
response += f"• Overdue: {invoice_summary.get('overdue_invoices', 0)}\n\n"
|
||||
|
||||
# Chats - always show
|
||||
response += "💬 CHATS:\n"
|
||||
response += f"• Unanswered: {len(unanswered_chats)}\n\n"
|
||||
|
||||
# Problems - show if any, otherwise skip
|
||||
if problems:
|
||||
response += "⚠️ ISSUES:\n"
|
||||
response += f"• Rooms with problems: {len(problems)}\n"
|
||||
for problem in problems[:3]:
|
||||
response += f" - Room {problem['room_number']}: {problem.get('description', 'Issue reported')}\n"
|
||||
|
||||
return response
|
||||
|
||||
elif intent == "app_info":
|
||||
# Comprehensive application information - formatted for admin
|
||||
app_knowledge = data.get("app_knowledge", {})
|
||||
role_name = self.role_access_service.role_name if self.role_access_service else "customer"
|
||||
|
||||
if role_name != "admin":
|
||||
response = "📱 Application Overview:\n\n"
|
||||
response += app_knowledge.get("overview", "This is a Hotel Booking and Management System.")
|
||||
response += "\n\n"
|
||||
role_info = app_knowledge.get("role_info", {})
|
||||
if role_info:
|
||||
response += f"Your Role: {role_name.upper()}\n"
|
||||
response += f"Description: {role_info.get('description', 'Standard user access')}\n"
|
||||
return response
|
||||
|
||||
# Admin gets full comprehensive information
|
||||
response = "📱 Hotel Booking & Management System (HBMS) - Complete Overview\n\n"
|
||||
response += "=" * 60 + "\n\n"
|
||||
|
||||
# Overview
|
||||
response += "📋 SYSTEM OVERVIEW:\n"
|
||||
response += app_knowledge.get("overview", "") + "\n\n"
|
||||
|
||||
# Core Features
|
||||
features = app_knowledge.get("features", {})
|
||||
if features:
|
||||
response += "🔧 CORE FEATURES & MODULES:\n\n"
|
||||
feature_count = 0
|
||||
for feature_name, feature_info in features.items():
|
||||
feature_count += 1
|
||||
feature_title = feature_name.replace("_", " ").title()
|
||||
response += f"{feature_count}. {feature_title}\n"
|
||||
response += f" {feature_info.get('description', 'N/A')}\n"
|
||||
|
||||
if feature_info.get('models'):
|
||||
models = feature_info.get('models', [])
|
||||
if models:
|
||||
response += f" Models: {', '.join(models[:5])}"
|
||||
if len(models) > 5:
|
||||
response += f" (+{len(models) - 5} more)"
|
||||
response += "\n"
|
||||
|
||||
if feature_info.get('statuses'):
|
||||
statuses = feature_info.get('statuses', [])
|
||||
response += f" Statuses: {', '.join(statuses)}\n"
|
||||
|
||||
response += "\n"
|
||||
|
||||
# User Roles
|
||||
role_capabilities = app_knowledge.get("role_capabilities", {})
|
||||
if role_capabilities:
|
||||
response += "👥 USER ROLES & PERMISSIONS:\n\n"
|
||||
for role_name_key, role_info in role_capabilities.items():
|
||||
response += f"• {role_name_key.upper()}\n"
|
||||
response += f" {role_info.get('description', '')}\n"
|
||||
|
||||
can_access = role_info.get('can_access', [])
|
||||
if can_access:
|
||||
response += f" Can Access: {len(can_access)} features\n"
|
||||
# Show first 5 capabilities
|
||||
for cap in can_access[:5]:
|
||||
response += f" - {cap}\n"
|
||||
if len(can_access) > 5:
|
||||
response += f" ... and {len(can_access) - 5} more\n"
|
||||
|
||||
cannot_access = role_info.get('cannot_access', [])
|
||||
if cannot_access:
|
||||
response += f" Restrictions: {len(cannot_access)} items\n"
|
||||
|
||||
response += "\n"
|
||||
|
||||
# Technology Stack
|
||||
response += "💻 TECHNOLOGY STACK:\n\n"
|
||||
response += "• Backend: FastAPI (Python)\n"
|
||||
response += "• Frontend: React/TypeScript\n"
|
||||
response += "• Database: SQLAlchemy ORM\n"
|
||||
response += "• AI: Self-learning assistant with pattern recognition\n"
|
||||
response += "• Authentication: JWT with MFA support\n"
|
||||
response += "• Security: Role-based access control, audit logging\n\n"
|
||||
|
||||
# Key Capabilities
|
||||
response += "✨ KEY CAPABILITIES:\n\n"
|
||||
response += "• Complete booking lifecycle management\n"
|
||||
response += "• Multi-role access control (Admin, Staff, Accountant, Customer)\n"
|
||||
response += "• Payment processing with multiple methods\n"
|
||||
response += "• Invoice generation and tracking\n"
|
||||
response += "• Loyalty program with points and rewards\n"
|
||||
response += "• Real-time chat support system\n"
|
||||
response += "• Room maintenance and housekeeping management\n"
|
||||
response += "• Guest profile and CRM features\n"
|
||||
response += "• Analytics and reporting\n"
|
||||
response += "• Content management (blog, pages, banners)\n"
|
||||
response += "• Email campaigns and marketing automation\n"
|
||||
response += "• Group booking management\n"
|
||||
response += "• Dynamic pricing and rate plans\n"
|
||||
response += "• Package deals (room + services)\n"
|
||||
response += "• Review and rating system\n"
|
||||
response += "• Audit logging and security compliance\n\n"
|
||||
|
||||
# Common Queries
|
||||
common_queries = app_knowledge.get("common_queries", {})
|
||||
if common_queries:
|
||||
response += "💬 COMMON QUERY CATEGORIES:\n\n"
|
||||
for category, queries in common_queries.items():
|
||||
response += f"• {category.title()}: {', '.join(queries[:3])}"
|
||||
if len(queries) > 3:
|
||||
response += f" (+{len(queries) - 3} more)"
|
||||
response += "\n"
|
||||
response += "\n"
|
||||
|
||||
response += "=" * 60 + "\n"
|
||||
response += "\n💡 TIP: Ask me about any specific feature, model, or capability for detailed information!"
|
||||
|
||||
return response
|
||||
|
||||
elif intent == "general_query":
|
||||
# Try to provide helpful response based on available data
|
||||
room_summary = data.get("room_summary", {})
|
||||
booking_summary = data.get("booking_summary", {})
|
||||
payment_summary = data.get("payment_summary", {})
|
||||
|
||||
response = "I can help you with information about:\n\n"
|
||||
response += "🏨 Rooms - availability, occupancy, status, problems\n"
|
||||
response += "📅 Bookings - active, upcoming, pending reservations\n"
|
||||
response += "💰 Payments - revenue, pending payments, transactions\n"
|
||||
response += "🧾 Invoices - status, overdue invoices\n"
|
||||
response += "💬 Chats - unanswered customer messages\n\n"
|
||||
|
||||
if room_summary:
|
||||
response += f"Quick Stats: {room_summary.get('total_rooms', 0)} total rooms, "
|
||||
response += f"{room_summary.get('occupied', 0)} occupied, "
|
||||
response += f"{room_summary.get('available', 0)} available.\n\n"
|
||||
|
||||
response += "Try asking:\n"
|
||||
response += "• 'How many rooms do we have?'\n"
|
||||
response += "• 'Show me occupied rooms'\n"
|
||||
response += "• 'What's our revenue?'\n"
|
||||
response += "• 'Are there any unanswered chats?'"
|
||||
|
||||
return response
|
||||
|
||||
else:
|
||||
return "I can help you with questions about rooms, bookings, invoices, payments, and chat messages. What would you like to know?"
|
||||
|
||||
def _apply_learned_pattern(self, pattern: AILearnedPattern, data: Dict, query: str) -> Optional[str]:
|
||||
"""Apply learned pattern to generate response"""
|
||||
try:
|
||||
response_template = pattern.response_template or ""
|
||||
|
||||
# Replace placeholders with actual data
|
||||
if "{data}" in response_template and data:
|
||||
# Inject relevant data into response
|
||||
data_str = json.dumps(data, indent=2, default=str)[:500]
|
||||
response = response_template.replace("{data}", data_str)
|
||||
else:
|
||||
response = response_template
|
||||
|
||||
return response[:2000] # Limit response length
|
||||
except Exception as e:
|
||||
logger.error(f"Error applying learned pattern: {str(e)}", exc_info=True)
|
||||
return None
|
||||
|
||||
382
Backend/src/ai/services/ai_knowledge_base.py
Normal file
382
Backend/src/ai/services/ai_knowledge_base.py
Normal file
@@ -0,0 +1,382 @@
|
||||
"""
|
||||
Enterprise AI Knowledge Base
|
||||
Comprehensive documentation of all application features, models, and capabilities
|
||||
"""
|
||||
|
||||
from typing import Dict, List, Any
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class AIKnowledgeBase:
|
||||
"""Enterprise Knowledge Base for AI Assistant - Documents entire application"""
|
||||
|
||||
APPLICATION_OVERVIEW = """
|
||||
This is a comprehensive Hotel Booking and Management System (HBMS) - an enterprise-grade
|
||||
solution for managing hotel operations, reservations, payments, guest services, and business analytics.
|
||||
|
||||
The system supports multiple user roles: Admin, Staff, Accountant, and Customer, each with
|
||||
different access levels and permissions.
|
||||
"""
|
||||
|
||||
# Core Models and Features
|
||||
FEATURES = {
|
||||
"booking_management": {
|
||||
"description": "Complete booking lifecycle management including creation, confirmation, check-in, check-out, and cancellation",
|
||||
"models": ["Booking", "BookingStatus"],
|
||||
"key_fields": ["booking_number", "user_id", "room_id", "check_in_date", "check_out_date",
|
||||
"num_guests", "total_price", "status", "special_requests"],
|
||||
"statuses": ["pending", "confirmed", "checked_in", "checked_out", "cancelled"],
|
||||
"relationships": ["User", "Room", "Payment", "Invoice", "ServiceUsage", "CheckInCheckOut"]
|
||||
},
|
||||
|
||||
"room_management": {
|
||||
"description": "Room inventory, availability, status tracking, and maintenance",
|
||||
"models": ["Room", "RoomType", "RoomStatus"],
|
||||
"key_fields": ["room_number", "floor", "status", "price", "capacity", "amenities", "room_size", "view"],
|
||||
"statuses": ["available", "occupied", "maintenance", "cleaning"],
|
||||
"relationships": ["RoomType", "Booking", "Review", "RoomMaintenance", "HousekeepingTask"]
|
||||
},
|
||||
|
||||
"payment_processing": {
|
||||
"description": "Payment handling for bookings with multiple payment methods and status tracking",
|
||||
"models": ["Payment", "PaymentMethod", "PaymentStatus", "PaymentType"],
|
||||
"key_fields": ["amount", "payment_method", "payment_type", "payment_status", "transaction_id", "payment_date"],
|
||||
"payment_methods": ["cash", "credit_card", "debit_card", "bank_transfer", "e_wallet", "stripe", "paypal", "borica"],
|
||||
"statuses": ["pending", "completed", "failed", "refunded"],
|
||||
"payment_types": ["full", "deposit", "remaining"],
|
||||
"relationships": ["Booking"]
|
||||
},
|
||||
|
||||
"invoice_management": {
|
||||
"description": "Invoice generation, tracking, and payment reconciliation",
|
||||
"models": ["Invoice", "InvoiceItem", "InvoiceStatus"],
|
||||
"key_fields": ["invoice_number", "total_amount", "subtotal", "tax_amount", "discount_amount",
|
||||
"balance_due", "status", "due_date", "paid_date"],
|
||||
"statuses": ["draft", "sent", "paid", "overdue", "cancelled"],
|
||||
"relationships": ["Booking", "User", "InvoiceItem"]
|
||||
},
|
||||
|
||||
"user_management": {
|
||||
"description": "User accounts, roles, authentication, and guest profiles",
|
||||
"models": ["User", "Role", "RefreshToken", "PasswordResetToken"],
|
||||
"key_fields": ["email", "full_name", "phone", "role_id", "is_active", "currency"],
|
||||
"roles": ["admin", "staff", "customer", "accountant"],
|
||||
"guest_fields": ["is_vip", "lifetime_value", "satisfaction_score", "total_visits", "last_visit_date"],
|
||||
"relationships": ["Role", "Booking", "Payment", "Invoice", "Review", "Favorite"]
|
||||
},
|
||||
|
||||
"loyalty_program": {
|
||||
"description": "Customer loyalty program with tiers, points, rewards, and referrals",
|
||||
"models": ["LoyaltyTier", "UserLoyalty", "LoyaltyPointTransaction", "LoyaltyReward",
|
||||
"RewardRedemption", "Referral"],
|
||||
"key_fields": ["tier_level", "points_balance", "lifetime_points", "referral_code"],
|
||||
"relationships": ["User", "Booking"]
|
||||
},
|
||||
|
||||
"service_management": {
|
||||
"description": "Additional hotel services like spa, restaurant, room service",
|
||||
"models": ["Service", "ServiceUsage", "ServiceBooking", "ServiceBookingItem", "ServicePayment"],
|
||||
"key_fields": ["service_name", "price", "category", "description"],
|
||||
"relationships": ["Booking"]
|
||||
},
|
||||
|
||||
"promotion_system": {
|
||||
"description": "Discount codes, promotional campaigns, and special offers",
|
||||
"models": ["Promotion"],
|
||||
"key_fields": ["code", "discount_type", "discount_value", "start_date", "end_date", "is_active"],
|
||||
"relationships": ["Booking"]
|
||||
},
|
||||
|
||||
"reviews_and_ratings": {
|
||||
"description": "Guest reviews and ratings for rooms and services",
|
||||
"models": ["Review"],
|
||||
"key_fields": ["rating", "comment", "is_verified"],
|
||||
"relationships": ["User", "Room"]
|
||||
},
|
||||
|
||||
"chat_system": {
|
||||
"description": "Real-time chat support between guests and staff",
|
||||
"models": ["Chat", "ChatMessage", "ChatStatus"],
|
||||
"key_fields": ["visitor_name", "visitor_email", "status", "message", "sender_type", "is_read"],
|
||||
"statuses": ["open", "closed"],
|
||||
"relationships": ["User"]
|
||||
},
|
||||
|
||||
"check_in_check_out": {
|
||||
"description": "Check-in and check-out process management",
|
||||
"models": ["CheckInCheckOut"],
|
||||
"key_fields": ["checked_in_at", "checked_out_at", "checked_in_by", "checked_out_by", "notes"],
|
||||
"relationships": ["Booking", "User"]
|
||||
},
|
||||
|
||||
"maintenance_tracking": {
|
||||
"description": "Room maintenance scheduling and tracking",
|
||||
"models": ["RoomMaintenance", "RoomInspection"],
|
||||
"key_fields": ["scheduled_start", "scheduled_end", "status", "description", "priority"],
|
||||
"relationships": ["Room"]
|
||||
},
|
||||
|
||||
"housekeeping": {
|
||||
"description": "Housekeeping task management and scheduling",
|
||||
"models": ["HousekeepingTask"],
|
||||
"key_fields": ["task_type", "status", "scheduled_time", "completed_at"],
|
||||
"relationships": ["Room"]
|
||||
},
|
||||
|
||||
"guest_management": {
|
||||
"description": "Comprehensive guest profile management and CRM features",
|
||||
"models": ["GuestNote", "GuestPreference", "GuestTag", "GuestSegment", "GuestCommunication"],
|
||||
"key_fields": ["notes", "preferences", "tags", "communication_history"],
|
||||
"relationships": ["User"]
|
||||
},
|
||||
|
||||
"group_bookings": {
|
||||
"description": "Group booking management for multiple rooms",
|
||||
"models": ["GroupBooking"],
|
||||
"key_fields": ["group_name", "contact_person", "total_rooms", "group_discount"],
|
||||
"relationships": ["Booking"]
|
||||
},
|
||||
|
||||
"rate_plans": {
|
||||
"description": "Dynamic pricing and rate plan management",
|
||||
"models": ["RatePlan"],
|
||||
"key_fields": ["name", "base_price", "season_multiplier", "valid_from", "valid_to"],
|
||||
"relationships": ["Booking"]
|
||||
},
|
||||
|
||||
"packages": {
|
||||
"description": "Pre-configured booking packages (room + services)",
|
||||
"models": ["Package"],
|
||||
"key_fields": ["name", "price", "includes", "valid_period"],
|
||||
"relationships": ["RoomType", "Service"]
|
||||
},
|
||||
|
||||
"blog_and_content": {
|
||||
"description": "Content management for hotel blog and marketing",
|
||||
"models": ["BlogPost", "Banner", "PageContent"],
|
||||
"key_fields": ["title", "content", "is_published", "published_at"],
|
||||
"relationships": []
|
||||
},
|
||||
|
||||
"analytics_and_reporting": {
|
||||
"description": "Business intelligence, analytics, and financial reporting",
|
||||
"features": ["Revenue reports", "Occupancy analytics", "Guest analytics", "Payment reports",
|
||||
"Invoice reports", "Booking trends", "Customer lifetime value"],
|
||||
"models": ["AuditLog"]
|
||||
},
|
||||
|
||||
"system_settings": {
|
||||
"description": "System-wide configuration and settings",
|
||||
"models": ["SystemSettings"],
|
||||
"key_fields": ["key", "value", "category"],
|
||||
"categories": ["general", "payment", "email", "company_info"]
|
||||
},
|
||||
|
||||
"security_and_compliance": {
|
||||
"description": "Security, audit logging, and GDPR compliance",
|
||||
"models": ["SecurityEvent", "AuditLog", "GDPRCompliance"],
|
||||
"features": ["Action audit trails", "Security event logging", "GDPR data requests",
|
||||
"Cookie consent management"]
|
||||
},
|
||||
|
||||
"notifications": {
|
||||
"description": "System notifications and alerts",
|
||||
"models": ["Notification"],
|
||||
"key_fields": ["type", "message", "is_read", "priority"],
|
||||
"relationships": ["User"]
|
||||
},
|
||||
|
||||
"workflows": {
|
||||
"description": "Automated workflow management for business processes",
|
||||
"models": ["Workflow"],
|
||||
"key_fields": ["name", "trigger", "actions", "conditions"],
|
||||
"relationships": []
|
||||
},
|
||||
|
||||
"email_campaigns": {
|
||||
"description": "Marketing email campaigns and automation",
|
||||
"models": ["EmailCampaign"],
|
||||
"key_fields": ["subject", "content", "recipient_segment", "scheduled_time"],
|
||||
"relationships": ["GuestSegment"]
|
||||
}
|
||||
}
|
||||
|
||||
ROLE_CAPABILITIES = {
|
||||
"admin": {
|
||||
"description": "Full system access with all administrative privileges",
|
||||
"can_access": [
|
||||
"All bookings (all customers)",
|
||||
"All rooms and room management",
|
||||
"All payments and financial data",
|
||||
"All invoices",
|
||||
"All users and user management",
|
||||
"System settings and configuration",
|
||||
"Analytics and reports",
|
||||
"Audit logs and security events",
|
||||
"Content management (blog, banners, pages)",
|
||||
"Promotion management",
|
||||
"Loyalty program administration",
|
||||
"Service management",
|
||||
"Maintenance and housekeeping",
|
||||
"Check-in/check-out operations",
|
||||
"Chat management",
|
||||
"Email campaigns",
|
||||
"Workflow management",
|
||||
"Group bookings",
|
||||
"Rate plans and packages"
|
||||
],
|
||||
"cannot_access": [],
|
||||
"restrictions": "None - full access"
|
||||
},
|
||||
|
||||
"accountant": {
|
||||
"description": "Financial data access for accounting and reporting",
|
||||
"can_access": [
|
||||
"All payments and payment history",
|
||||
"All invoices and invoice management",
|
||||
"Financial reports and analytics",
|
||||
"Revenue summaries",
|
||||
"Payment reconciliation",
|
||||
"Invoice generation and editing",
|
||||
"Booking financial details (for invoicing)",
|
||||
"Customer payment history",
|
||||
"Refund processing",
|
||||
"Financial dashboard statistics"
|
||||
],
|
||||
"cannot_access": [
|
||||
"User personal information (except payment-related)",
|
||||
"System settings",
|
||||
"Content management",
|
||||
"Room operational details (unless related to billing)",
|
||||
"Security and audit logs (financial audit access only)"
|
||||
],
|
||||
"restrictions": "Financial and payment data only"
|
||||
},
|
||||
|
||||
"staff": {
|
||||
"description": "Operational staff with access to daily hotel operations",
|
||||
"can_access": [
|
||||
"All bookings and booking management",
|
||||
"Room status and availability",
|
||||
"Check-in and check-out operations",
|
||||
"Guest information (for operations)",
|
||||
"Room maintenance and housekeeping tasks",
|
||||
"Service bookings and service usage",
|
||||
"Chat system for customer support",
|
||||
"Basic booking reports",
|
||||
"Occupancy status",
|
||||
"Group bookings",
|
||||
"Guest preferences and notes (operational)"
|
||||
],
|
||||
"cannot_access": [
|
||||
"Financial reports and analytics",
|
||||
"System settings",
|
||||
"User management (except guest profile updates)",
|
||||
"Content management",
|
||||
"Email campaigns",
|
||||
"Workflow management",
|
||||
"Full payment details (can view payment status only)",
|
||||
"Invoice editing (view only)"
|
||||
],
|
||||
"restrictions": "Operational data only, no financial or system administration"
|
||||
},
|
||||
|
||||
"customer": {
|
||||
"description": "Hotel guests with access to their own booking and account information",
|
||||
"can_access": [
|
||||
"Own bookings (view, cancel)",
|
||||
"Own booking details",
|
||||
"Own invoices (view only)",
|
||||
"Own payment history",
|
||||
"Own profile information",
|
||||
"Room browsing and availability (public)",
|
||||
"Service browsing",
|
||||
"Booking creation",
|
||||
"Review submission",
|
||||
"Favorite rooms",
|
||||
"Loyalty points and rewards",
|
||||
"Referral code usage",
|
||||
"Chat with support"
|
||||
],
|
||||
"cannot_access": [
|
||||
"Other customers' bookings or information",
|
||||
"All bookings overview",
|
||||
"Financial reports",
|
||||
"System settings",
|
||||
"Room management",
|
||||
"Staff operations",
|
||||
"Admin features",
|
||||
"Other users' data"
|
||||
],
|
||||
"restrictions": "Only own data and customer-facing features"
|
||||
}
|
||||
}
|
||||
|
||||
COMMON_QUERIES = {
|
||||
"booking": [
|
||||
"booking status", "check-in date", "check-out date", "booking number",
|
||||
"cancel booking", "modify booking", "upcoming bookings", "past bookings"
|
||||
],
|
||||
"room": [
|
||||
"available rooms", "room types", "room amenities", "room prices",
|
||||
"room availability", "occupied rooms", "room status"
|
||||
],
|
||||
"payment": [
|
||||
"payment status", "payment history", "payment methods", "refund",
|
||||
"outstanding balance", "payment due", "transaction"
|
||||
],
|
||||
"invoice": [
|
||||
"invoice status", "invoice number", "invoice amount", "due date",
|
||||
"paid invoice", "outstanding invoice", "invoice download"
|
||||
],
|
||||
"loyalty": [
|
||||
"loyalty points", "loyalty tier", "rewards", "referral code",
|
||||
"points balance", "points history"
|
||||
],
|
||||
"general": [
|
||||
"hotel information", "contact", "policies", "services", "amenities",
|
||||
"location", "FAQ", "help"
|
||||
]
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def get_application_knowledge(cls) -> Dict[str, Any]:
|
||||
"""Get comprehensive application knowledge for AI context"""
|
||||
return {
|
||||
"overview": cls.APPLICATION_OVERVIEW,
|
||||
"features": cls.FEATURES,
|
||||
"role_capabilities": cls.ROLE_CAPABILITIES,
|
||||
"common_queries": cls.COMMON_QUERIES
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def get_role_info(cls, role_name: str) -> Dict[str, Any]:
|
||||
"""Get information about a specific role"""
|
||||
return cls.ROLE_CAPABILITIES.get(role_name.lower(), {})
|
||||
|
||||
@classmethod
|
||||
def get_feature_info(cls, feature_name: str) -> Dict[str, Any]:
|
||||
"""Get information about a specific feature"""
|
||||
return cls.FEATURES.get(feature_name.lower(), {})
|
||||
|
||||
@classmethod
|
||||
def can_role_access_feature(cls, role_name: str, feature: str) -> bool:
|
||||
"""Check if a role can access a specific feature"""
|
||||
role_info = cls.ROLE_CAPABILITIES.get(role_name.lower(), {})
|
||||
can_access = role_info.get("can_access", [])
|
||||
|
||||
# Admin can access everything
|
||||
if role_name.lower() == "admin":
|
||||
return True
|
||||
|
||||
# Check if feature is in can_access list
|
||||
feature_keywords = feature.lower().split("_")
|
||||
for accessible in can_access:
|
||||
accessible_lower = accessible.lower()
|
||||
if any(keyword in accessible_lower for keyword in feature_keywords):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
818
Backend/src/ai/services/ai_learning_service.py
Normal file
818
Backend/src/ai/services/ai_learning_service.py
Normal file
@@ -0,0 +1,818 @@
|
||||
"""
|
||||
AI Learning Service - Self-learning and pattern recognition system
|
||||
Learns from conversations, stores patterns, and improves responses over time
|
||||
"""
|
||||
|
||||
from typing import Dict, List, Optional, Any, Tuple
|
||||
from datetime import datetime, timedelta
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy import and_, or_, func, desc
|
||||
import json
|
||||
import re
|
||||
import logging
|
||||
from collections import Counter
|
||||
|
||||
from ..models.ai_conversation import (
|
||||
AIConversation, AIConversationFeedback, AILearnedPattern,
|
||||
AIKnowledgeEntry, AITrainingMetrics
|
||||
)
|
||||
from ...shared.config.logging_config import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class AILearningService:
|
||||
"""Service for AI self-learning and pattern recognition"""
|
||||
|
||||
def __init__(self, db: Session):
|
||||
self.db = db
|
||||
|
||||
def save_conversation(
|
||||
self,
|
||||
user_id: int,
|
||||
user_query: str,
|
||||
ai_response: str,
|
||||
intent: str,
|
||||
context_used: Optional[Dict] = None,
|
||||
user_role: Optional[str] = None,
|
||||
session_id: Optional[str] = None,
|
||||
response_time_ms: Optional[int] = None
|
||||
) -> AIConversation:
|
||||
"""Save conversation for learning"""
|
||||
try:
|
||||
conversation = AIConversation(
|
||||
user_id=user_id,
|
||||
session_id=session_id,
|
||||
user_query=user_query,
|
||||
ai_response=ai_response,
|
||||
intent=intent,
|
||||
context_used=context_used or {},
|
||||
user_role=user_role,
|
||||
response_time_ms=response_time_ms
|
||||
)
|
||||
self.db.add(conversation)
|
||||
self.db.commit()
|
||||
self.db.refresh(conversation)
|
||||
|
||||
# Trigger learning process in background
|
||||
self._learn_from_conversation(conversation)
|
||||
|
||||
return conversation
|
||||
except Exception as e:
|
||||
logger.error(f"Error saving conversation: {str(e)}", exc_info=True)
|
||||
self.db.rollback()
|
||||
raise
|
||||
|
||||
def save_feedback(
|
||||
self,
|
||||
conversation_id: int,
|
||||
user_id: int,
|
||||
rating: Optional[int] = None,
|
||||
is_helpful: Optional[bool] = None,
|
||||
is_correct: Optional[bool] = None,
|
||||
feedback_text: Optional[str] = None,
|
||||
correction: Optional[str] = None
|
||||
) -> AIConversationFeedback:
|
||||
"""Save user feedback for learning"""
|
||||
try:
|
||||
feedback = AIConversationFeedback(
|
||||
conversation_id=conversation_id,
|
||||
user_id=user_id,
|
||||
rating=rating,
|
||||
is_helpful=is_helpful,
|
||||
is_correct=is_correct,
|
||||
feedback_text=feedback_text,
|
||||
correction=correction
|
||||
)
|
||||
self.db.add(feedback)
|
||||
|
||||
# Update conversation helpfulness
|
||||
conversation = self.db.query(AIConversation).filter(
|
||||
AIConversation.id == conversation_id
|
||||
).first()
|
||||
if conversation:
|
||||
conversation.is_helpful = is_helpful
|
||||
|
||||
self.db.commit()
|
||||
self.db.refresh(feedback)
|
||||
|
||||
# Trigger learning from feedback
|
||||
if feedback.is_correct == False or correction:
|
||||
self._learn_from_feedback(conversation, feedback)
|
||||
|
||||
return feedback
|
||||
except Exception as e:
|
||||
logger.error(f"Error saving feedback: {str(e)}", exc_info=True)
|
||||
self.db.rollback()
|
||||
raise
|
||||
|
||||
def find_similar_queries(self, query: str, intent: Optional[str] = None, limit: int = 5) -> List[Dict[str, Any]]:
|
||||
"""Find similar past queries to learn from"""
|
||||
try:
|
||||
# Extract keywords from query
|
||||
keywords = self._extract_keywords(query)
|
||||
|
||||
# Search for similar queries
|
||||
query_obj = self.db.query(AIConversation).filter(
|
||||
AIConversation.user_query.ilike(f"%{query[:50]}%")
|
||||
)
|
||||
|
||||
if intent:
|
||||
query_obj = query_obj.filter(AIConversation.intent == intent)
|
||||
|
||||
similar = query_obj.order_by(desc(AIConversation.created_at)).limit(limit).all()
|
||||
|
||||
results = []
|
||||
for conv in similar:
|
||||
results.append({
|
||||
"query": conv.user_query,
|
||||
"response": conv.ai_response,
|
||||
"intent": conv.intent,
|
||||
"is_helpful": conv.is_helpful,
|
||||
"created_at": conv.created_at.isoformat()
|
||||
})
|
||||
|
||||
return results
|
||||
except Exception as e:
|
||||
logger.error(f"Error finding similar queries: {str(e)}", exc_info=True)
|
||||
return []
|
||||
|
||||
def get_learned_pattern(self, query: str, user_role: Optional[str] = None) -> Optional[AILearnedPattern]:
|
||||
"""Get learned pattern that matches query"""
|
||||
try:
|
||||
keywords = self._extract_keywords(query)
|
||||
|
||||
# Search patterns by keywords
|
||||
patterns = self.db.query(AILearnedPattern).filter(
|
||||
and_(
|
||||
AILearnedPattern.is_active == True,
|
||||
or_(*[AILearnedPattern.pattern_keywords.contains(kw) for kw in keywords[:5]])
|
||||
)
|
||||
)
|
||||
|
||||
if user_role:
|
||||
patterns = patterns.filter(
|
||||
or_(
|
||||
AILearnedPattern.user_role == user_role,
|
||||
AILearnedPattern.user_role.is_(None)
|
||||
)
|
||||
)
|
||||
|
||||
# Order by confidence and usage
|
||||
patterns = patterns.order_by(
|
||||
desc(AILearnedPattern.confidence_score),
|
||||
desc(AILearnedPattern.usage_count)
|
||||
).limit(1).all()
|
||||
|
||||
if patterns:
|
||||
pattern = patterns[0]
|
||||
# Update usage stats
|
||||
pattern.usage_count += 1
|
||||
pattern.last_used_at = datetime.utcnow()
|
||||
self.db.commit()
|
||||
return pattern
|
||||
|
||||
return None
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting learned pattern: {str(e)}", exc_info=True)
|
||||
return None
|
||||
|
||||
def get_knowledge_entry(self, query: str, user_role: Optional[str] = None) -> Optional[Dict[str, Any]]:
|
||||
"""Get knowledge base entry that matches query"""
|
||||
try:
|
||||
keywords = self._extract_keywords(query)
|
||||
query_lower = query.lower()
|
||||
|
||||
# Search knowledge entries
|
||||
entries = self.db.query(AIKnowledgeEntry).filter(
|
||||
and_(
|
||||
or_(
|
||||
AIKnowledgeEntry.question.ilike(f"%{query[:50]}%"),
|
||||
*[AIKnowledgeEntry.keywords.contains([kw]) for kw in keywords[:3]]
|
||||
),
|
||||
or_(
|
||||
AIKnowledgeEntry.is_verified == True,
|
||||
AIKnowledgeEntry.confidence >= 70.0
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
if user_role:
|
||||
entries = entries.filter(
|
||||
or_(
|
||||
AIKnowledgeEntry.user_role == user_role,
|
||||
AIKnowledgeEntry.user_role.is_(None)
|
||||
)
|
||||
)
|
||||
|
||||
entries = entries.order_by(
|
||||
desc(AIKnowledgeEntry.confidence),
|
||||
desc(AIKnowledgeEntry.success_count)
|
||||
).limit(1).all()
|
||||
|
||||
if entries:
|
||||
entry = entries[0]
|
||||
# Update usage stats
|
||||
entry.usage_count += 1
|
||||
entry.last_used_at = datetime.utcnow()
|
||||
self.db.commit()
|
||||
|
||||
return {
|
||||
"topic": entry.topic,
|
||||
"question": entry.question,
|
||||
"answer": entry.answer,
|
||||
"confidence": float(entry.confidence)
|
||||
}
|
||||
|
||||
return None
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting knowledge entry: {str(e)}", exc_info=True)
|
||||
return None
|
||||
|
||||
def _learn_from_conversation(self, conversation: AIConversation):
|
||||
"""Learn patterns from a conversation - automatically learns from all conversations"""
|
||||
try:
|
||||
# Always extract keywords and patterns (learn from all conversations)
|
||||
keywords = self._extract_keywords(conversation.user_query)
|
||||
|
||||
# Check if similar pattern exists
|
||||
existing_pattern = self.get_learned_pattern(
|
||||
conversation.user_query,
|
||||
conversation.user_role
|
||||
)
|
||||
|
||||
if existing_pattern:
|
||||
# Update existing pattern confidence
|
||||
existing_pattern.success_count += 1
|
||||
existing_pattern.usage_count += 1
|
||||
|
||||
# Adjust confidence based on helpfulness
|
||||
if conversation.is_helpful is True:
|
||||
existing_pattern.confidence_score = min(
|
||||
100.0,
|
||||
existing_pattern.confidence_score + 1.0
|
||||
)
|
||||
elif conversation.is_helpful is False:
|
||||
existing_pattern.confidence_score = max(
|
||||
0.0,
|
||||
existing_pattern.confidence_score - 2.0
|
||||
)
|
||||
else:
|
||||
# Neutral - slight increase
|
||||
existing_pattern.confidence_score = min(
|
||||
100.0,
|
||||
existing_pattern.confidence_score + 0.3
|
||||
)
|
||||
|
||||
existing_pattern.last_used_at = datetime.utcnow()
|
||||
else:
|
||||
# Create new pattern - learn from all conversations
|
||||
initial_confidence = 60.0 if conversation.is_helpful is True else 50.0
|
||||
if conversation.is_helpful is False:
|
||||
initial_confidence = 30.0 # Lower confidence for unhelpful patterns
|
||||
|
||||
pattern = AILearnedPattern(
|
||||
pattern_keywords=json.dumps(keywords),
|
||||
query_pattern=conversation.user_query[:200],
|
||||
intent=conversation.intent or "general_query",
|
||||
response_template=conversation.ai_response[:500],
|
||||
context_keys=conversation.context_used,
|
||||
confidence_score=initial_confidence,
|
||||
usage_count=1,
|
||||
success_count=1 if conversation.is_helpful is not False else 0,
|
||||
source_conversation_id=conversation.id,
|
||||
user_role=conversation.user_role,
|
||||
last_used_at=datetime.utcnow()
|
||||
)
|
||||
self.db.add(pattern)
|
||||
|
||||
# Automatically extract knowledge from every conversation
|
||||
# This enables the AI to build its knowledge base autonomously
|
||||
self._auto_extract_knowledge(conversation)
|
||||
|
||||
self.db.commit()
|
||||
except Exception as e:
|
||||
logger.error(f"Error learning from conversation: {str(e)}", exc_info=True)
|
||||
self.db.rollback()
|
||||
|
||||
def _learn_from_feedback(self, conversation: AIConversation, feedback: AIConversationFeedback):
|
||||
"""Learn from user feedback and corrections"""
|
||||
try:
|
||||
# If user provided correction, create knowledge entry
|
||||
if feedback.correction:
|
||||
knowledge = AIKnowledgeEntry(
|
||||
topic=self._extract_topic(conversation.user_query),
|
||||
question=conversation.user_query,
|
||||
answer=feedback.correction,
|
||||
keywords=self._extract_keywords(conversation.user_query),
|
||||
related_intent=conversation.intent,
|
||||
source='user_feedback',
|
||||
confidence=75.0,
|
||||
user_role=conversation.user_role,
|
||||
created_by_user_id=feedback.user_id
|
||||
)
|
||||
self.db.add(knowledge)
|
||||
|
||||
# Update pattern confidence based on feedback
|
||||
patterns = self.db.query(AILearnedPattern).filter(
|
||||
AILearnedPattern.source_conversation_id == conversation.id
|
||||
).all()
|
||||
|
||||
for pattern in patterns:
|
||||
if feedback.is_helpful:
|
||||
pattern.confidence_score = min(100.0, pattern.confidence_score + 2.0)
|
||||
pattern.success_count += 1
|
||||
elif feedback.is_helpful == False:
|
||||
pattern.confidence_score = max(0.0, pattern.confidence_score - 5.0)
|
||||
|
||||
if feedback.is_correct == False:
|
||||
pattern.is_active = False # Deactivate incorrect patterns
|
||||
|
||||
self.db.commit()
|
||||
except Exception as e:
|
||||
logger.error(f"Error learning from feedback: {str(e)}", exc_info=True)
|
||||
self.db.rollback()
|
||||
|
||||
def _extract_keywords(self, text: str) -> List[str]:
|
||||
"""Extract keywords from text"""
|
||||
# Remove common stop words
|
||||
stop_words = {
|
||||
'the', 'a', 'an', 'and', 'or', 'but', 'in', 'on', 'at', 'to', 'for',
|
||||
'of', 'with', 'by', 'from', 'as', 'is', 'are', 'was', 'were', 'be',
|
||||
'been', 'have', 'has', 'had', 'do', 'does', 'did', 'will', 'would',
|
||||
'should', 'could', 'may', 'might', 'can', 'this', 'that', 'these',
|
||||
'those', 'i', 'you', 'he', 'she', 'it', 'we', 'they', 'me', 'him',
|
||||
'her', 'us', 'them', 'my', 'your', 'his', 'her', 'its', 'our', 'their',
|
||||
'what', 'which', 'who', 'whom', 'whose', 'where', 'when', 'why', 'how',
|
||||
'show', 'tell', 'give', 'get', 'find', 'search', 'list', 'all'
|
||||
}
|
||||
|
||||
# Extract words
|
||||
words = re.findall(r'\b[a-z]+\b', text.lower())
|
||||
keywords = [w for w in words if w not in stop_words and len(w) > 2]
|
||||
|
||||
# Count frequency and return top keywords
|
||||
word_freq = Counter(keywords)
|
||||
return [word for word, _ in word_freq.most_common(10)]
|
||||
|
||||
def _extract_topic(self, query: str) -> str:
|
||||
"""Extract topic from query"""
|
||||
# Simple topic extraction - can be enhanced
|
||||
keywords = self._extract_keywords(query)
|
||||
if keywords:
|
||||
return keywords[0].title()
|
||||
return "General"
|
||||
|
||||
def train_from_history(self, days: int = 7) -> Dict[str, Any]:
|
||||
"""Train AI from recent conversation history"""
|
||||
try:
|
||||
cutoff_date = datetime.utcnow() - timedelta(days=days)
|
||||
|
||||
# Get conversations from last N days
|
||||
conversations = self.db.query(AIConversation).filter(
|
||||
AIConversation.created_at >= cutoff_date
|
||||
).all()
|
||||
|
||||
trained_patterns = 0
|
||||
trained_knowledge = 0
|
||||
improved_patterns = 0
|
||||
|
||||
for conv in conversations:
|
||||
# Learn from helpful conversations
|
||||
if conv.is_helpful is True:
|
||||
self._learn_from_conversation(conv)
|
||||
trained_patterns += 1
|
||||
|
||||
# Get feedback for this conversation
|
||||
feedbacks = self.db.query(AIConversationFeedback).filter(
|
||||
AIConversationFeedback.conversation_id == conv.id
|
||||
).all()
|
||||
|
||||
for feedback in feedbacks:
|
||||
if feedback.correction:
|
||||
# Create knowledge entry from correction
|
||||
knowledge = AIKnowledgeEntry(
|
||||
topic=self._extract_topic(conv.user_query),
|
||||
question=conv.user_query,
|
||||
answer=feedback.correction,
|
||||
keywords=self._extract_keywords(conv.user_query),
|
||||
related_intent=conv.intent,
|
||||
source='training',
|
||||
confidence=80.0,
|
||||
user_role=conv.user_role
|
||||
)
|
||||
self.db.add(knowledge)
|
||||
trained_knowledge += 1
|
||||
|
||||
self.db.commit()
|
||||
|
||||
# Update training metrics
|
||||
self._update_training_metrics()
|
||||
|
||||
return {
|
||||
"conversations_analyzed": len(conversations),
|
||||
"patterns_trained": trained_patterns,
|
||||
"knowledge_entries_created": trained_knowledge,
|
||||
"improved_patterns": improved_patterns
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"Error in training: {str(e)}", exc_info=True)
|
||||
self.db.rollback()
|
||||
return {"error": str(e)}
|
||||
|
||||
def _update_training_metrics(self):
|
||||
"""Update training metrics"""
|
||||
try:
|
||||
today = datetime.utcnow().date()
|
||||
|
||||
# Calculate metrics
|
||||
total_conversations = self.db.query(AIConversation).count()
|
||||
total_patterns = self.db.query(AILearnedPattern).filter(
|
||||
AILearnedPattern.is_active == True
|
||||
).count()
|
||||
total_knowledge = self.db.query(AIKnowledgeEntry).filter(
|
||||
AIKnowledgeEntry.is_verified == True
|
||||
).count()
|
||||
|
||||
# Average response time
|
||||
avg_response_time = self.db.query(
|
||||
func.avg(AIConversation.response_time_ms)
|
||||
).filter(
|
||||
AIConversation.response_time_ms.isnot(None)
|
||||
).scalar()
|
||||
|
||||
# Average rating
|
||||
avg_rating = self.db.query(
|
||||
func.avg(AIConversationFeedback.rating)
|
||||
).filter(
|
||||
AIConversationFeedback.rating.isnot(None)
|
||||
).scalar()
|
||||
|
||||
# Helpful rate
|
||||
total_with_feedback = self.db.query(AIConversation).filter(
|
||||
AIConversation.is_helpful.isnot(None)
|
||||
).count()
|
||||
helpful_count = self.db.query(AIConversation).filter(
|
||||
AIConversation.is_helpful == True
|
||||
).count()
|
||||
helpful_rate = (helpful_count / total_with_feedback * 100) if total_with_feedback > 0 else None
|
||||
|
||||
# Create or update metrics
|
||||
metrics = self.db.query(AITrainingMetrics).filter(
|
||||
func.date(AITrainingMetrics.metric_date) == today
|
||||
).first()
|
||||
|
||||
if not metrics:
|
||||
metrics = AITrainingMetrics(metric_date=datetime.utcnow())
|
||||
self.db.add(metrics)
|
||||
|
||||
metrics.total_conversations = total_conversations
|
||||
metrics.total_patterns_learned = total_patterns
|
||||
metrics.total_knowledge_entries = total_knowledge
|
||||
metrics.average_response_time_ms = int(avg_response_time) if avg_response_time else None
|
||||
metrics.average_rating = float(avg_rating) if avg_rating else None
|
||||
metrics.helpful_rate = float(helpful_rate) if helpful_rate else None
|
||||
|
||||
self.db.commit()
|
||||
except Exception as e:
|
||||
logger.error(f"Error updating training metrics: {str(e)}", exc_info=True)
|
||||
self.db.rollback()
|
||||
|
||||
def auto_train_from_all_conversations(self) -> Dict[str, Any]:
|
||||
"""Automatically train from all conversations - self-learning method"""
|
||||
try:
|
||||
logger.info("Starting automatic self-training from all conversations...")
|
||||
|
||||
# Get all conversations that haven't been processed for learning
|
||||
# Process conversations from last 30 days by default
|
||||
cutoff_date = datetime.utcnow() - timedelta(days=30)
|
||||
conversations = self.db.query(AIConversation).filter(
|
||||
AIConversation.created_at >= cutoff_date
|
||||
).order_by(desc(AIConversation.created_at)).all()
|
||||
|
||||
trained_patterns = 0
|
||||
created_knowledge = 0
|
||||
improved_patterns = 0
|
||||
analyzed_conversations = 0
|
||||
|
||||
for conv in conversations:
|
||||
analyzed_conversations += 1
|
||||
|
||||
# Always learn from conversations (even without explicit feedback)
|
||||
# The AI learns from patterns in successful responses
|
||||
if conv.is_helpful is not False: # Learn from helpful or neutral conversations
|
||||
# Extract and save knowledge automatically
|
||||
self._auto_extract_knowledge(conv)
|
||||
created_knowledge += 1
|
||||
|
||||
# Learn patterns from all conversations
|
||||
self._learn_from_conversation(conv)
|
||||
trained_patterns += 1
|
||||
|
||||
# Auto-optimize patterns based on usage and success
|
||||
improved_patterns = self._auto_optimize_patterns()
|
||||
|
||||
# Auto-merge similar patterns
|
||||
merged = self._auto_merge_similar_patterns()
|
||||
|
||||
# Update metrics
|
||||
self._update_training_metrics()
|
||||
|
||||
logger.info(f"Auto-training completed: {analyzed_conversations} conversations analyzed, "
|
||||
f"{trained_patterns} patterns trained, {created_knowledge} knowledge entries created, "
|
||||
f"{improved_patterns} patterns improved, {merged} patterns merged")
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"conversations_analyzed": analyzed_conversations,
|
||||
"patterns_trained": trained_patterns,
|
||||
"knowledge_entries_created": created_knowledge,
|
||||
"patterns_improved": improved_patterns,
|
||||
"patterns_merged": merged,
|
||||
"timestamp": datetime.utcnow().isoformat()
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"Error in auto-training: {str(e)}", exc_info=True)
|
||||
self.db.rollback()
|
||||
return {"status": "error", "error": str(e)}
|
||||
|
||||
def _auto_extract_knowledge(self, conversation: AIConversation):
|
||||
"""Automatically extract knowledge from conversations"""
|
||||
try:
|
||||
# Check if similar knowledge already exists
|
||||
keywords = self._extract_keywords(conversation.user_query)
|
||||
existing = self.db.query(AIKnowledgeEntry).filter(
|
||||
and_(
|
||||
AIKnowledgeEntry.question.ilike(f"%{conversation.user_query[:50]}%"),
|
||||
or_(
|
||||
AIKnowledgeEntry.user_role == conversation.user_role,
|
||||
AIKnowledgeEntry.user_role.is_(None)
|
||||
)
|
||||
)
|
||||
).first()
|
||||
|
||||
if not existing:
|
||||
# Create new knowledge entry (only for helpful or neutral conversations)
|
||||
# Don't learn from explicitly unhelpful responses
|
||||
if conversation.is_helpful is not False:
|
||||
knowledge = AIKnowledgeEntry(
|
||||
topic=self._extract_topic(conversation.user_query),
|
||||
question=conversation.user_query,
|
||||
answer=conversation.ai_response,
|
||||
keywords=keywords,
|
||||
related_intent=conversation.intent,
|
||||
source='auto_learned',
|
||||
confidence=60.0 if conversation.is_helpful is True else 50.0,
|
||||
user_role=conversation.user_role,
|
||||
is_verified=False # Auto-learned entries need verification
|
||||
)
|
||||
self.db.add(knowledge)
|
||||
else:
|
||||
# Update existing knowledge if this response is better
|
||||
# Increase confidence if response is marked as helpful
|
||||
existing.usage_count += 1
|
||||
if conversation.is_helpful is True:
|
||||
existing.confidence = min(100.0, existing.confidence + 5.0)
|
||||
existing.success_count += 1
|
||||
# Update answer if confidence is high enough
|
||||
if existing.confidence >= 80.0:
|
||||
existing.answer = conversation.ai_response
|
||||
existing.updated_at = datetime.utcnow()
|
||||
elif conversation.is_helpful is False:
|
||||
existing.confidence = max(20.0, existing.confidence - 3.0)
|
||||
else:
|
||||
# Neutral - slight increase
|
||||
existing.confidence = min(100.0, existing.confidence + 1.0)
|
||||
existing.success_count += 1
|
||||
|
||||
existing.last_used_at = datetime.utcnow()
|
||||
|
||||
# Don't commit here - let the caller commit
|
||||
except Exception as e:
|
||||
logger.error(f"Error auto-extracting knowledge: {str(e)}", exc_info=True)
|
||||
# Don't rollback here - let the caller handle it
|
||||
|
||||
def _auto_optimize_patterns(self) -> int:
|
||||
"""Automatically optimize patterns based on performance"""
|
||||
try:
|
||||
improved = 0
|
||||
|
||||
# Get all active patterns
|
||||
patterns = self.db.query(AILearnedPattern).filter(
|
||||
AILearnedPattern.is_active == True
|
||||
).all()
|
||||
|
||||
for pattern in patterns:
|
||||
# Calculate success rate
|
||||
if pattern.usage_count > 0:
|
||||
success_rate = (pattern.success_count / pattern.usage_count) * 100
|
||||
|
||||
# Adjust confidence based on success rate
|
||||
if success_rate >= 80:
|
||||
# High success rate - increase confidence
|
||||
pattern.confidence_score = min(100.0, pattern.confidence_score + 1.0)
|
||||
improved += 1
|
||||
elif success_rate < 50 and pattern.usage_count >= 5:
|
||||
# Low success rate - decrease confidence or deactivate
|
||||
pattern.confidence_score = max(0.0, pattern.confidence_score - 2.0)
|
||||
if pattern.confidence_score < 30:
|
||||
pattern.is_active = False
|
||||
logger.info(f"Deactivated low-performing pattern {pattern.id}")
|
||||
improved += 1
|
||||
|
||||
# Update last used timestamp
|
||||
pattern.updated_at = datetime.utcnow()
|
||||
|
||||
self.db.commit()
|
||||
return improved
|
||||
except Exception as e:
|
||||
logger.error(f"Error optimizing patterns: {str(e)}", exc_info=True)
|
||||
self.db.rollback()
|
||||
return 0
|
||||
|
||||
def _auto_merge_similar_patterns(self) -> int:
|
||||
"""Automatically merge similar patterns to reduce redundancy"""
|
||||
try:
|
||||
merged = 0
|
||||
|
||||
# Get patterns with same intent and role
|
||||
patterns_by_intent = {}
|
||||
all_patterns = self.db.query(AILearnedPattern).filter(
|
||||
AILearnedPattern.is_active == True
|
||||
).all()
|
||||
|
||||
for pattern in all_patterns:
|
||||
key = (pattern.intent, pattern.user_role)
|
||||
if key not in patterns_by_intent:
|
||||
patterns_by_intent[key] = []
|
||||
patterns_by_intent[key].append(pattern)
|
||||
|
||||
# Merge similar patterns within each intent group
|
||||
for (intent, role), patterns in patterns_by_intent.items():
|
||||
if len(patterns) < 2:
|
||||
continue
|
||||
|
||||
# Sort by confidence and usage
|
||||
patterns.sort(key=lambda p: (p.confidence_score, p.usage_count), reverse=True)
|
||||
|
||||
# Keep the best pattern and merge others into it
|
||||
best_pattern = patterns[0]
|
||||
for pattern in patterns[1:]:
|
||||
# Check if patterns are similar (same keywords or similar query pattern)
|
||||
best_keywords = set(json.loads(best_pattern.pattern_keywords) if best_pattern.pattern_keywords else [])
|
||||
pattern_keywords = set(json.loads(pattern.pattern_keywords) if pattern.pattern_keywords else [])
|
||||
|
||||
# If 70% keywords overlap, merge them
|
||||
if best_keywords and pattern_keywords:
|
||||
overlap = len(best_keywords & pattern_keywords) / len(best_keywords | pattern_keywords)
|
||||
if overlap >= 0.7:
|
||||
# Merge: combine usage counts and update confidence
|
||||
best_pattern.usage_count += pattern.usage_count
|
||||
best_pattern.success_count += pattern.success_count
|
||||
best_pattern.confidence_score = (
|
||||
(best_pattern.confidence_score * best_pattern.usage_count +
|
||||
pattern.confidence_score * pattern.usage_count) /
|
||||
(best_pattern.usage_count + pattern.usage_count)
|
||||
)
|
||||
|
||||
# Deactivate the merged pattern
|
||||
pattern.is_active = False
|
||||
merged += 1
|
||||
logger.info(f"Merged pattern {pattern.id} into {best_pattern.id}")
|
||||
|
||||
self.db.commit()
|
||||
return merged
|
||||
except Exception as e:
|
||||
logger.error(f"Error merging patterns: {str(e)}", exc_info=True)
|
||||
self.db.rollback()
|
||||
return 0
|
||||
|
||||
def auto_analyze_and_improve(self) -> Dict[str, Any]:
|
||||
"""Automatically analyze performance and improve - self-improvement method"""
|
||||
try:
|
||||
logger.info("Starting automatic self-analysis and improvement...")
|
||||
|
||||
# Analyze conversation quality
|
||||
recent_conversations = self.db.query(AIConversation).filter(
|
||||
AIConversation.created_at >= datetime.utcnow() - timedelta(days=7)
|
||||
).all()
|
||||
|
||||
# Find patterns in successful vs unsuccessful conversations
|
||||
successful_queries = []
|
||||
unsuccessful_queries = []
|
||||
|
||||
for conv in recent_conversations:
|
||||
if conv.is_helpful is True:
|
||||
successful_queries.append(conv.user_query)
|
||||
elif conv.is_helpful is False:
|
||||
unsuccessful_queries.append(conv.user_query)
|
||||
|
||||
# Extract common patterns from successful queries
|
||||
if successful_queries:
|
||||
common_success_keywords = self._find_common_keywords(successful_queries)
|
||||
logger.info(f"Common keywords in successful queries: {common_success_keywords[:10]}")
|
||||
|
||||
# Identify problematic patterns
|
||||
if unsuccessful_queries:
|
||||
common_failure_keywords = self._find_common_keywords(unsuccessful_queries)
|
||||
logger.info(f"Common keywords in unsuccessful queries: {common_failure_keywords[:10]}")
|
||||
|
||||
# Improve knowledge entries based on usage
|
||||
improved_knowledge = self._improve_knowledge_entries()
|
||||
|
||||
# Clean up old inactive patterns
|
||||
cleaned = self._cleanup_old_patterns()
|
||||
|
||||
# Update metrics
|
||||
self._update_training_metrics()
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"knowledge_entries_improved": improved_knowledge,
|
||||
"old_patterns_cleaned": cleaned,
|
||||
"timestamp": datetime.utcnow().isoformat()
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"Error in auto-analysis: {str(e)}", exc_info=True)
|
||||
return {"status": "error", "error": str(e)}
|
||||
|
||||
def _find_common_keywords(self, queries: List[str]) -> List[str]:
|
||||
"""Find common keywords across multiple queries"""
|
||||
all_keywords = []
|
||||
for query in queries:
|
||||
keywords = self._extract_keywords(query)
|
||||
all_keywords.extend(keywords)
|
||||
|
||||
# Count frequency
|
||||
keyword_freq = Counter(all_keywords)
|
||||
return [word for word, _ in keyword_freq.most_common(20)]
|
||||
|
||||
def _improve_knowledge_entries(self) -> int:
|
||||
"""Improve knowledge entries based on usage statistics"""
|
||||
try:
|
||||
improved = 0
|
||||
|
||||
# Get knowledge entries with usage data
|
||||
entries = self.db.query(AIKnowledgeEntry).filter(
|
||||
AIKnowledgeEntry.usage_count > 0
|
||||
).all()
|
||||
|
||||
for entry in entries:
|
||||
# Calculate success rate
|
||||
if entry.usage_count > 0:
|
||||
success_rate = (entry.success_count / entry.usage_count) * 100
|
||||
|
||||
# Auto-verify entries with high success rate
|
||||
if success_rate >= 85 and entry.usage_count >= 5 and not entry.is_verified:
|
||||
entry.is_verified = True
|
||||
entry.confidence = min(100.0, entry.confidence + 10.0)
|
||||
improved += 1
|
||||
logger.info(f"Auto-verified knowledge entry {entry.id} (success rate: {success_rate:.1f}%)")
|
||||
|
||||
# Increase confidence for successful entries
|
||||
elif success_rate >= 70:
|
||||
entry.confidence = min(100.0, entry.confidence + 2.0)
|
||||
improved += 1
|
||||
|
||||
# Decrease confidence for unsuccessful entries
|
||||
elif success_rate < 40 and entry.usage_count >= 3:
|
||||
entry.confidence = max(20.0, entry.confidence - 5.0)
|
||||
improved += 1
|
||||
|
||||
self.db.commit()
|
||||
return improved
|
||||
except Exception as e:
|
||||
logger.error(f"Error improving knowledge entries: {str(e)}", exc_info=True)
|
||||
self.db.rollback()
|
||||
return 0
|
||||
|
||||
def _cleanup_old_patterns(self) -> int:
|
||||
"""Clean up old inactive or low-performing patterns"""
|
||||
try:
|
||||
cutoff_date = datetime.utcnow() - timedelta(days=90)
|
||||
|
||||
# Find patterns that haven't been used in 90 days and have low confidence
|
||||
old_patterns = self.db.query(AILearnedPattern).filter(
|
||||
and_(
|
||||
or_(
|
||||
AILearnedPattern.last_used_at < cutoff_date,
|
||||
AILearnedPattern.last_used_at.is_(None)
|
||||
),
|
||||
AILearnedPattern.confidence_score < 40.0,
|
||||
AILearnedPattern.is_active == True
|
||||
)
|
||||
).all()
|
||||
|
||||
cleaned = 0
|
||||
for pattern in old_patterns:
|
||||
pattern.is_active = False
|
||||
cleaned += 1
|
||||
|
||||
self.db.commit()
|
||||
logger.info(f"Cleaned up {cleaned} old inactive patterns")
|
||||
return cleaned
|
||||
except Exception as e:
|
||||
logger.error(f"Error cleaning up patterns: {str(e)}", exc_info=True)
|
||||
self.db.rollback()
|
||||
return 0
|
||||
|
||||
383
Backend/src/ai/services/ai_role_access_service.py
Normal file
383
Backend/src/ai/services/ai_role_access_service.py
Normal file
@@ -0,0 +1,383 @@
|
||||
"""
|
||||
Enterprise Role-Based Access Control Service for AI Assistant
|
||||
Filters and controls data access based on user roles
|
||||
"""
|
||||
|
||||
from typing import Dict, List, Optional, Any
|
||||
from datetime import datetime, timedelta
|
||||
from sqlalchemy.orm import Session, joinedload
|
||||
from sqlalchemy import and_, or_, func
|
||||
import logging
|
||||
|
||||
from ...auth.models.user import User
|
||||
from ...auth.models.role import Role
|
||||
from ...bookings.models.booking import Booking, BookingStatus
|
||||
from ...payments.models.invoice import Invoice
|
||||
from ...payments.models.payment import Payment, PaymentStatus
|
||||
from ...rooms.models.room import Room, RoomStatus
|
||||
from ...shared.config.logging_config import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class AIRoleAccessService:
|
||||
"""Role-based data access control for AI Assistant"""
|
||||
|
||||
def __init__(self, db: Session, current_user: User):
|
||||
self.db = db
|
||||
self.current_user = current_user
|
||||
|
||||
# Load user role - try from relationship first, then query
|
||||
self.user_role = None
|
||||
if hasattr(current_user, 'role') and current_user.role:
|
||||
self.user_role = current_user.role
|
||||
|
||||
if not self.user_role:
|
||||
self.user_role = db.query(Role).filter(Role.id == current_user.role_id).first()
|
||||
|
||||
if not self.user_role:
|
||||
# Fallback to customer role if not found
|
||||
logger.warning(f"Role not found for user {current_user.id}, defaulting to customer")
|
||||
self.user_role = db.query(Role).filter(Role.name == 'customer').first()
|
||||
|
||||
self.role_name = self.user_role.name.lower() if self.user_role else "customer"
|
||||
|
||||
def get_user_bookings(self, limit: int = 10) -> List[Dict[str, Any]]:
|
||||
"""Get bookings based on role permissions"""
|
||||
if self.role_name in ["admin", "staff"]:
|
||||
# Admin and staff can see all bookings
|
||||
bookings = self.db.query(Booking).options(
|
||||
joinedload(Booking.user),
|
||||
joinedload(Booking.room).joinedload(Room.room_type)
|
||||
).order_by(Booking.created_at.desc()).limit(limit).all()
|
||||
elif self.role_name == "accountant":
|
||||
# Accountant can see all bookings (for invoicing purposes)
|
||||
bookings = self.db.query(Booking).options(
|
||||
joinedload(Booking.user),
|
||||
joinedload(Booking.room).joinedload(Room.room_type)
|
||||
).order_by(Booking.created_at.desc()).limit(limit).all()
|
||||
else:
|
||||
# Customers can only see their own bookings
|
||||
bookings = self.db.query(Booking).options(
|
||||
joinedload(Booking.user),
|
||||
joinedload(Booking.room).joinedload(Room.room_type)
|
||||
).filter(Booking.user_id == self.current_user.id).order_by(
|
||||
Booking.created_at.desc()
|
||||
).limit(limit).all()
|
||||
|
||||
result = []
|
||||
for booking in bookings:
|
||||
booking_dict = {
|
||||
"booking_number": booking.booking_number,
|
||||
"status": booking.status.value if hasattr(booking.status, 'value') else str(booking.status),
|
||||
"check_in": booking.check_in_date.isoformat(),
|
||||
"check_out": booking.check_out_date.isoformat(),
|
||||
"num_guests": booking.num_guests,
|
||||
"total_price": float(booking.total_price) if booking.total_price else 0,
|
||||
}
|
||||
|
||||
# Add user info based on role
|
||||
if self.role_name in ["admin", "staff", "accountant"]:
|
||||
booking_dict["guest_name"] = booking.user.full_name if booking.user else "Unknown"
|
||||
booking_dict["guest_email"] = booking.user.email if booking.user else None
|
||||
else:
|
||||
booking_dict["guest_name"] = "Your booking"
|
||||
|
||||
# Add room info
|
||||
if booking.room:
|
||||
booking_dict["room_number"] = booking.room.room_number
|
||||
if booking.room.room_type:
|
||||
booking_dict["room_type"] = booking.room.room_type.name
|
||||
|
||||
result.append(booking_dict)
|
||||
|
||||
return result
|
||||
|
||||
def get_user_invoices(self, limit: int = 10) -> List[Dict[str, Any]]:
|
||||
"""Get invoices based on role permissions"""
|
||||
if self.role_name in ["admin", "accountant"]:
|
||||
# Admin and accountant can see all invoices
|
||||
invoices = self.db.query(Invoice).options(
|
||||
joinedload(Invoice.user),
|
||||
joinedload(Invoice.booking)
|
||||
).order_by(Invoice.created_at.desc()).limit(limit).all()
|
||||
elif self.role_name == "staff":
|
||||
# Staff can view invoices but not edit
|
||||
invoices = self.db.query(Invoice).options(
|
||||
joinedload(Invoice.user),
|
||||
joinedload(Invoice.booking)
|
||||
).order_by(Invoice.created_at.desc()).limit(limit).all()
|
||||
else:
|
||||
# Customers can only see their own invoices
|
||||
invoices = self.db.query(Invoice).options(
|
||||
joinedload(Invoice.user),
|
||||
joinedload(Invoice.booking)
|
||||
).filter(Invoice.user_id == self.current_user.id).order_by(
|
||||
Invoice.created_at.desc()
|
||||
).limit(limit).all()
|
||||
|
||||
result = []
|
||||
for invoice in invoices:
|
||||
invoice_dict = {
|
||||
"invoice_number": invoice.invoice_number,
|
||||
"status": invoice.status.value if hasattr(invoice.status, 'value') else str(invoice.status),
|
||||
"total_amount": float(invoice.total_amount) if invoice.total_amount else 0,
|
||||
"balance_due": float(invoice.balance_due) if invoice.balance_due else 0,
|
||||
"due_date": invoice.due_date.isoformat() if invoice.due_date else None,
|
||||
"paid_date": invoice.paid_date.isoformat() if invoice.paid_date else None,
|
||||
}
|
||||
|
||||
# Add user info based on role
|
||||
if self.role_name in ["admin", "staff", "accountant"]:
|
||||
invoice_dict["customer_name"] = invoice.customer_name if hasattr(invoice, 'customer_name') else (
|
||||
invoice.user.full_name if invoice.user else "Unknown"
|
||||
)
|
||||
invoice_dict["customer_email"] = invoice.customer_email if hasattr(invoice, 'customer_email') else (
|
||||
invoice.user.email if invoice.user else None
|
||||
)
|
||||
else:
|
||||
invoice_dict["customer_name"] = "Your invoice"
|
||||
|
||||
if invoice.booking:
|
||||
invoice_dict["booking_number"] = invoice.booking.booking_number
|
||||
|
||||
result.append(invoice_dict)
|
||||
|
||||
return result
|
||||
|
||||
def get_user_payments(self, limit: Optional[int] = 10) -> List[Dict[str, Any]]:
|
||||
"""Get payments based on role permissions"""
|
||||
if self.role_name in ["admin", "accountant"]:
|
||||
# Admin and accountant can see all payments
|
||||
query = self.db.query(Payment).options(
|
||||
joinedload(Payment.booking).joinedload(Booking.user)
|
||||
).order_by(Payment.created_at.desc())
|
||||
if limit:
|
||||
payments = query.limit(limit).all()
|
||||
else:
|
||||
payments = query.all()
|
||||
elif self.role_name == "staff":
|
||||
# Staff can see payment status but limited details
|
||||
query = self.db.query(Payment).options(
|
||||
joinedload(Payment.booking).joinedload(Booking.user)
|
||||
).order_by(Payment.created_at.desc())
|
||||
if limit:
|
||||
payments = query.limit(limit).all()
|
||||
else:
|
||||
payments = query.all()
|
||||
else:
|
||||
# Customers can only see their own payments
|
||||
query = self.db.query(Payment).options(
|
||||
joinedload(Payment.booking)
|
||||
).join(Booking).filter(Booking.user_id == self.current_user.id).order_by(
|
||||
Payment.created_at.desc()
|
||||
)
|
||||
if limit:
|
||||
payments = query.limit(limit).all()
|
||||
else:
|
||||
payments = query.all()
|
||||
|
||||
result = []
|
||||
for payment in payments:
|
||||
payment_type = payment.payment_type.value if hasattr(payment.payment_type, 'value') else str(payment.payment_type)
|
||||
|
||||
# Format payment type with deposit percentage if available
|
||||
if payment_type == 'deposit' and payment.deposit_percentage:
|
||||
payment_type_display = f"Deposit ({payment.deposit_percentage}%)"
|
||||
elif payment_type == 'deposit':
|
||||
payment_type_display = "Deposit (20%)" # Default
|
||||
elif payment_type == 'full':
|
||||
payment_type_display = "Full Payment"
|
||||
elif payment_type == 'remaining':
|
||||
payment_type_display = "Remaining"
|
||||
else:
|
||||
payment_type_display = payment_type
|
||||
|
||||
payment_dict = {
|
||||
"id": payment.id,
|
||||
"amount": float(payment.amount) if payment.amount else 0,
|
||||
"payment_method": payment.payment_method.value if hasattr(payment.payment_method, 'value') else str(payment.payment_method),
|
||||
"payment_type": payment_type,
|
||||
"payment_type_display": payment_type_display,
|
||||
"payment_status": payment.payment_status.value if hasattr(payment.payment_status, 'value') else str(payment.payment_status),
|
||||
"payment_date": payment.payment_date.isoformat() if payment.payment_date else None,
|
||||
"transaction_id": payment.transaction_id or f"PAY-{payment.id}",
|
||||
"deposit_percentage": payment.deposit_percentage,
|
||||
}
|
||||
|
||||
if payment.booking:
|
||||
payment_dict["booking_number"] = payment.booking.booking_number
|
||||
|
||||
# Add user info based on role
|
||||
if self.role_name in ["admin", "staff", "accountant"] and payment.booking.user:
|
||||
payment_dict["customer_name"] = payment.booking.user.full_name
|
||||
payment_dict["customer_email"] = payment.booking.user.email
|
||||
else:
|
||||
payment_dict["customer_name"] = "Your payment"
|
||||
|
||||
result.append(payment_dict)
|
||||
|
||||
return result
|
||||
|
||||
def get_all_payments(self) -> List[Dict[str, Any]]:
|
||||
"""Get all payments without limit - for admin and accountant only"""
|
||||
if self.role_name not in ["admin", "accountant"]:
|
||||
return []
|
||||
return self.get_user_payments(limit=None)
|
||||
|
||||
def get_room_status_summary(self) -> Dict[str, Any]:
|
||||
"""Get room status summary based on role permissions"""
|
||||
total_rooms = self.db.query(Room).count()
|
||||
available_rooms = self.db.query(Room).filter(Room.status == RoomStatus.available).count()
|
||||
occupied_rooms = self.db.query(Room).filter(Room.status == RoomStatus.occupied).count()
|
||||
|
||||
result = {
|
||||
"total_rooms": total_rooms,
|
||||
"available": available_rooms,
|
||||
"occupied": occupied_rooms,
|
||||
}
|
||||
|
||||
# Additional details for admin/staff
|
||||
if self.role_name in ["admin", "staff"]:
|
||||
maintenance_rooms = self.db.query(Room).filter(Room.status == RoomStatus.maintenance).count()
|
||||
cleaning_rooms = self.db.query(Room).filter(Room.status == RoomStatus.cleaning).count()
|
||||
|
||||
today = datetime.utcnow().date()
|
||||
rooms_with_bookings = self.db.query(Booking).filter(
|
||||
and_(
|
||||
Booking.status != BookingStatus.cancelled,
|
||||
Booking.check_in_date <= today,
|
||||
Booking.check_out_date > today
|
||||
)
|
||||
).count()
|
||||
|
||||
result.update({
|
||||
"maintenance": maintenance_rooms,
|
||||
"cleaning": cleaning_rooms,
|
||||
"rooms_with_active_bookings": rooms_with_bookings,
|
||||
"occupancy_rate": round((occupied_rooms / total_rooms * 100) if total_rooms > 0 else 0, 2)
|
||||
})
|
||||
else:
|
||||
# Customers get simplified room availability info
|
||||
result["occupancy_rate"] = round((occupied_rooms / total_rooms * 100) if total_rooms > 0 else 0, 2)
|
||||
|
||||
return result
|
||||
|
||||
def get_payment_summary(self, days: int = 30) -> Optional[Dict[str, Any]]:
|
||||
"""Get payment summary - only for admin, accountant, and staff"""
|
||||
if self.role_name not in ["admin", "accountant", "staff"]:
|
||||
return None # Customers cannot access payment summaries
|
||||
|
||||
today = datetime.utcnow().date()
|
||||
start_date = today - timedelta(days=days)
|
||||
|
||||
# Total payments in period
|
||||
total_payments = self.db.query(func.sum(Payment.amount)).filter(
|
||||
and_(
|
||||
Payment.payment_status == PaymentStatus.completed,
|
||||
func.date(Payment.payment_date) >= start_date,
|
||||
func.date(Payment.payment_date) <= today
|
||||
)
|
||||
).scalar() or 0
|
||||
|
||||
# Pending payments
|
||||
pending_payments = self.db.query(func.sum(Payment.amount)).filter(
|
||||
Payment.payment_status == PaymentStatus.pending
|
||||
).scalar() or 0
|
||||
|
||||
# Payment count
|
||||
completed_count = self.db.query(Payment).filter(
|
||||
and_(
|
||||
Payment.payment_status == PaymentStatus.completed,
|
||||
func.date(Payment.payment_date) >= start_date
|
||||
)
|
||||
).count()
|
||||
|
||||
result = {
|
||||
"total_revenue": float(total_payments),
|
||||
"pending_amount": float(pending_payments),
|
||||
"completed_payments": completed_count,
|
||||
"period_days": days
|
||||
}
|
||||
|
||||
# Additional details for admin and accountant
|
||||
if self.role_name in ["admin", "accountant"]:
|
||||
failed_count = self.db.query(Payment).filter(
|
||||
Payment.payment_status == PaymentStatus.failed
|
||||
).count()
|
||||
result["failed_payments"] = failed_count
|
||||
|
||||
return result
|
||||
|
||||
def get_invoice_summary(self) -> Optional[Dict[str, Any]]:
|
||||
"""Get invoice summary - only for admin, accountant, and staff"""
|
||||
if self.role_name not in ["admin", "accountant", "staff"]:
|
||||
return None # Customers cannot access invoice summaries
|
||||
|
||||
total_invoices = self.db.query(Invoice).count()
|
||||
|
||||
# Group by status
|
||||
status_counts = {}
|
||||
invoices = self.db.query(Invoice).all()
|
||||
for invoice in invoices:
|
||||
status = invoice.status.value if hasattr(invoice.status, 'value') else str(invoice.status)
|
||||
status_counts[status] = status_counts.get(status, 0) + 1
|
||||
|
||||
# Overdue invoices
|
||||
today = datetime.utcnow().date()
|
||||
overdue_count = self.db.query(Invoice).filter(
|
||||
and_(
|
||||
Invoice.due_date < today,
|
||||
Invoice.status != 'paid'
|
||||
)
|
||||
).count()
|
||||
|
||||
result = {
|
||||
"total_invoices": total_invoices,
|
||||
"status_breakdown": status_counts,
|
||||
"overdue_invoices": overdue_count
|
||||
}
|
||||
|
||||
return result
|
||||
|
||||
def can_access_feature(self, feature: str) -> bool:
|
||||
"""Check if current user can access a feature"""
|
||||
if self.role_name == "admin":
|
||||
return True
|
||||
|
||||
feature_lower = feature.lower()
|
||||
|
||||
# Define feature access rules
|
||||
if feature_lower in ["payment_summary", "invoice_summary", "financial_reports"]:
|
||||
return self.role_name in ["admin", "accountant", "staff"]
|
||||
|
||||
if feature_lower in ["all_bookings", "all_users", "system_settings"]:
|
||||
return self.role_name == "admin"
|
||||
|
||||
if feature_lower in ["operational_reports", "room_management", "maintenance"]:
|
||||
return self.role_name in ["admin", "staff"]
|
||||
|
||||
# Default: allow if it's user's own data
|
||||
return True
|
||||
|
||||
def filter_sensitive_data(self, data: Dict[str, Any], data_type: str) -> Dict[str, Any]:
|
||||
"""Filter sensitive data based on role"""
|
||||
if self.role_name == "admin":
|
||||
return data # Admin sees everything
|
||||
|
||||
filtered = data.copy()
|
||||
|
||||
# Remove sensitive financial data for staff
|
||||
if self.role_name == "staff" and data_type in ["payment", "invoice"]:
|
||||
if "transaction_id" in filtered:
|
||||
filtered["transaction_id"] = "***" if filtered.get("transaction_id") else None
|
||||
if "payment_method_details" in filtered:
|
||||
del filtered["payment_method_details"]
|
||||
|
||||
# Remove other users' personal info for non-admin roles
|
||||
if self.role_name != "admin" and data_type in ["booking", "invoice", "payment"]:
|
||||
# Only show own data, not other users' details
|
||||
pass # This is handled at query level
|
||||
|
||||
return filtered
|
||||
|
||||
171
Backend/src/ai/services/ai_training_scheduler.py
Normal file
171
Backend/src/ai/services/ai_training_scheduler.py
Normal file
@@ -0,0 +1,171 @@
|
||||
"""
|
||||
AI Training Scheduler - Automatic background training for AI self-learning
|
||||
Runs periodic training tasks to improve AI performance autonomously
|
||||
"""
|
||||
|
||||
import threading
|
||||
import time
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Optional
|
||||
from sqlalchemy.orm import Session
|
||||
import logging
|
||||
|
||||
from ...shared.config.database import get_db
|
||||
from ...shared.config.logging_config import get_logger
|
||||
from .ai_learning_service import AILearningService
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class AITrainingScheduler:
|
||||
"""Background scheduler for automatic AI training"""
|
||||
|
||||
def __init__(self):
|
||||
self.running = False
|
||||
self.thread: Optional[threading.Thread] = None
|
||||
self.training_interval_hours = 6 # Train every 6 hours
|
||||
self.analysis_interval_hours = 24 # Analyze every 24 hours
|
||||
self.last_training_time: Optional[datetime] = None
|
||||
self.last_analysis_time: Optional[datetime] = None
|
||||
|
||||
def start(self):
|
||||
"""Start the background training scheduler"""
|
||||
if self.running:
|
||||
logger.warning("AI Training Scheduler is already running")
|
||||
return
|
||||
|
||||
self.running = True
|
||||
self.thread = threading.Thread(target=self._scheduler_loop, daemon=True)
|
||||
self.thread.start()
|
||||
logger.info("AI Training Scheduler started - automatic self-learning enabled")
|
||||
|
||||
def stop(self):
|
||||
"""Stop the background training scheduler"""
|
||||
if not self.running:
|
||||
return
|
||||
|
||||
self.running = False
|
||||
if self.thread:
|
||||
self.thread.join(timeout=5.0)
|
||||
logger.info("AI Training Scheduler stopped")
|
||||
|
||||
def _scheduler_loop(self):
|
||||
"""Main scheduler loop that runs training tasks periodically"""
|
||||
logger.info("AI Training Scheduler loop started")
|
||||
|
||||
# Run initial training after 1 minute (to let app fully start)
|
||||
time.sleep(60)
|
||||
|
||||
while self.running:
|
||||
try:
|
||||
current_time = datetime.utcnow()
|
||||
|
||||
# Check if it's time for training
|
||||
should_train = False
|
||||
if self.last_training_time is None:
|
||||
should_train = True
|
||||
else:
|
||||
time_since_training = current_time - self.last_training_time
|
||||
if time_since_training >= timedelta(hours=self.training_interval_hours):
|
||||
should_train = True
|
||||
|
||||
if should_train:
|
||||
logger.info("Starting scheduled automatic training...")
|
||||
self._run_training()
|
||||
self.last_training_time = current_time
|
||||
|
||||
# Check if it's time for analysis
|
||||
should_analyze = False
|
||||
if self.last_analysis_time is None:
|
||||
should_analyze = True
|
||||
else:
|
||||
time_since_analysis = current_time - self.last_analysis_time
|
||||
if time_since_analysis >= timedelta(hours=self.analysis_interval_hours):
|
||||
should_analyze = True
|
||||
|
||||
if should_analyze:
|
||||
logger.info("Starting scheduled automatic analysis...")
|
||||
self._run_analysis()
|
||||
self.last_analysis_time = current_time
|
||||
|
||||
# Sleep for 1 hour before checking again
|
||||
time.sleep(3600)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error in scheduler loop: {str(e)}", exc_info=True)
|
||||
# Sleep for 10 minutes before retrying on error
|
||||
time.sleep(600)
|
||||
|
||||
def _run_training(self):
|
||||
"""Run automatic training from conversations"""
|
||||
try:
|
||||
# Get database session
|
||||
db_gen = get_db()
|
||||
db = next(db_gen)
|
||||
|
||||
try:
|
||||
learning_service = AILearningService(db)
|
||||
result = learning_service.auto_train_from_all_conversations()
|
||||
|
||||
if result.get("status") == "success":
|
||||
logger.info(
|
||||
f"Automatic training completed: "
|
||||
f"{result.get('conversations_analyzed', 0)} conversations analyzed, "
|
||||
f"{result.get('patterns_trained', 0)} patterns trained, "
|
||||
f"{result.get('knowledge_entries_created', 0)} knowledge entries created"
|
||||
)
|
||||
else:
|
||||
logger.warning(f"Training completed with errors: {result.get('error', 'Unknown error')}")
|
||||
finally:
|
||||
db.close()
|
||||
except Exception as e:
|
||||
logger.error(f"Error running automatic training: {str(e)}", exc_info=True)
|
||||
|
||||
def _run_analysis(self):
|
||||
"""Run automatic analysis and improvement"""
|
||||
try:
|
||||
# Get database session
|
||||
db_gen = get_db()
|
||||
db = next(db_gen)
|
||||
|
||||
try:
|
||||
learning_service = AILearningService(db)
|
||||
result = learning_service.auto_analyze_and_improve()
|
||||
|
||||
if result.get("status") == "success":
|
||||
logger.info(
|
||||
f"Automatic analysis completed: "
|
||||
f"{result.get('knowledge_entries_improved', 0)} knowledge entries improved, "
|
||||
f"{result.get('old_patterns_cleaned', 0)} old patterns cleaned"
|
||||
)
|
||||
else:
|
||||
logger.warning(f"Analysis completed with errors: {result.get('error', 'Unknown error')}")
|
||||
finally:
|
||||
db.close()
|
||||
except Exception as e:
|
||||
logger.error(f"Error running automatic analysis: {str(e)}", exc_info=True)
|
||||
|
||||
def trigger_manual_training(self):
|
||||
"""Manually trigger training (for testing or on-demand training)"""
|
||||
logger.info("Manual training triggered")
|
||||
self._run_training()
|
||||
self.last_training_time = datetime.utcnow()
|
||||
|
||||
def trigger_manual_analysis(self):
|
||||
"""Manually trigger analysis (for testing or on-demand analysis)"""
|
||||
logger.info("Manual analysis triggered")
|
||||
self._run_analysis()
|
||||
self.last_analysis_time = datetime.utcnow()
|
||||
|
||||
|
||||
# Global scheduler instance
|
||||
_global_scheduler: Optional[AITrainingScheduler] = None
|
||||
|
||||
|
||||
def get_training_scheduler() -> AITrainingScheduler:
|
||||
"""Get or create the global training scheduler instance"""
|
||||
global _global_scheduler
|
||||
if _global_scheduler is None:
|
||||
_global_scheduler = AITrainingScheduler()
|
||||
return _global_scheduler
|
||||
|
||||
0
Backend/src/analytics/__init__.py
Normal file
0
Backend/src/analytics/__init__.py
Normal file
0
Backend/src/analytics/models/__init__.py
Normal file
0
Backend/src/analytics/models/__init__.py
Normal file
@@ -1,7 +1,7 @@
|
||||
from sqlalchemy import Column, Integer, String, Text, DateTime, ForeignKey, JSON
|
||||
from sqlalchemy.orm import relationship
|
||||
from datetime import datetime
|
||||
from ..config.database import Base
|
||||
from ...shared.config.database import Base
|
||||
|
||||
class AuditLog(Base):
|
||||
__tablename__ = 'audit_logs'
|
||||
0
Backend/src/analytics/routes/__init__.py
Normal file
0
Backend/src/analytics/routes/__init__.py
Normal file
@@ -1,9 +1,9 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from sqlalchemy.orm import Session
|
||||
from typing import Optional
|
||||
from ..config.database import get_db
|
||||
from ..middleware.auth import authorize_roles, get_current_user
|
||||
from ..models.user import User
|
||||
from ...shared.config.database import get_db
|
||||
from ...security.middleware.auth import authorize_roles, get_current_user
|
||||
from ...auth.models.user import User
|
||||
from ..services.analytics_service import AnalyticsService
|
||||
|
||||
router = APIRouter(prefix='/analytics', tags=['analytics'])
|
||||
@@ -3,9 +3,9 @@ from sqlalchemy.orm import Session
|
||||
from sqlalchemy import desc, or_, func
|
||||
from typing import Optional
|
||||
from datetime import datetime
|
||||
from ..config.database import get_db
|
||||
from ..middleware.auth import get_current_user, authorize_roles
|
||||
from ..models.user import User
|
||||
from ...shared.config.database import get_db
|
||||
from ...security.middleware.auth import get_current_user, authorize_roles
|
||||
from ...auth.models.user import User
|
||||
from ..models.audit_log import AuditLog
|
||||
router = APIRouter(prefix='/audit-logs', tags=['audit-logs'])
|
||||
|
||||
@@ -3,15 +3,15 @@ from sqlalchemy.orm import Session, load_only, joinedload
|
||||
from sqlalchemy import func, and_
|
||||
from typing import Optional
|
||||
from datetime import datetime, timedelta
|
||||
from ..config.database import get_db
|
||||
from ..middleware.auth import get_current_user, authorize_roles
|
||||
from ..models.user import User
|
||||
from ..models.booking import Booking, BookingStatus
|
||||
from ..models.payment import Payment, PaymentStatus
|
||||
from ..models.room import Room, RoomStatus
|
||||
from ..models.service_usage import ServiceUsage
|
||||
from ..models.service import Service
|
||||
from ..utils.response_helpers import success_response
|
||||
from ...shared.config.database import get_db
|
||||
from ...security.middleware.auth import get_current_user, authorize_roles
|
||||
from ...auth.models.user import User
|
||||
from ...bookings.models.booking import Booking, BookingStatus
|
||||
from ...payments.models.payment import Payment, PaymentStatus
|
||||
from ...rooms.models.room import Room, RoomStatus
|
||||
from ...hotel_services.models.service_usage import ServiceUsage
|
||||
from ...hotel_services.models.service import Service
|
||||
from ...shared.utils.response_helpers import success_response
|
||||
router = APIRouter(prefix='/reports', tags=['reports'])
|
||||
|
||||
@router.get('')
|
||||
0
Backend/src/analytics/schemas/__init__.py
Normal file
0
Backend/src/analytics/schemas/__init__.py
Normal file
0
Backend/src/analytics/services/__init__.py
Normal file
0
Backend/src/analytics/services/__init__.py
Normal file
@@ -2,15 +2,15 @@ from sqlalchemy.orm import Session, load_only
|
||||
from sqlalchemy import func, and_, or_, case, extract, distinct
|
||||
from typing import Optional, Dict, List, Any
|
||||
from datetime import datetime, timedelta, date
|
||||
from ..models.booking import Booking, BookingStatus
|
||||
from ..models.payment import Payment, PaymentStatus, PaymentMethod
|
||||
from ..models.room import Room, RoomStatus
|
||||
from ..models.room_type import RoomType
|
||||
from ..models.user import User
|
||||
from ..models.service_usage import ServiceUsage
|
||||
from ..models.service import Service
|
||||
from ..models.review import Review, ReviewStatus
|
||||
from ..models.invoice import Invoice
|
||||
from ...bookings.models.booking import Booking, BookingStatus
|
||||
from ...payments.models.payment import Payment, PaymentStatus, PaymentMethod
|
||||
from ...rooms.models.room import Room, RoomStatus
|
||||
from ...rooms.models.room_type import RoomType
|
||||
from ...auth.models.user import User
|
||||
from ...hotel_services.models.service_usage import ServiceUsage
|
||||
from ...hotel_services.models.service import Service
|
||||
from ...reviews.models.review import Review, ReviewStatus
|
||||
from ...payments.models.invoice import Invoice
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -290,7 +290,7 @@ class AnalyticsService:
|
||||
start_date = end_date - timedelta(days=30)
|
||||
|
||||
# Get staff users
|
||||
from ..models.role import Role
|
||||
from ...auth.models.role import Role
|
||||
staff_users = db.query(User).join(Role, User.role_id == Role.id).filter(
|
||||
or_(Role.name == 'staff', Role.name == 'admin')
|
||||
).all()
|
||||
@@ -2,7 +2,7 @@ from sqlalchemy.orm import Session
|
||||
from typing import Optional, Dict, Any
|
||||
from datetime import datetime
|
||||
from ..models.audit_log import AuditLog
|
||||
from ..config.logging_config import get_logger
|
||||
from ...shared.config.logging_config import get_logger
|
||||
logger = get_logger(__name__)
|
||||
|
||||
class AuditService:
|
||||
0
Backend/src/auth/__init__.py
Normal file
0
Backend/src/auth/__init__.py
Normal file
0
Backend/src/auth/models/__init__.py
Normal file
0
Backend/src/auth/models/__init__.py
Normal file
@@ -1,7 +1,7 @@
|
||||
from sqlalchemy import Column, Integer, String, DateTime, Boolean, ForeignKey
|
||||
from sqlalchemy.orm import relationship
|
||||
from datetime import datetime
|
||||
from ..config.database import Base
|
||||
from ...shared.config.database import Base
|
||||
|
||||
class PasswordResetToken(Base):
|
||||
__tablename__ = 'password_reset_tokens'
|
||||
@@ -1,7 +1,7 @@
|
||||
from sqlalchemy import Column, Integer, String, DateTime, ForeignKey
|
||||
from sqlalchemy.orm import relationship
|
||||
from datetime import datetime
|
||||
from ..config.database import Base
|
||||
from ...shared.config.database import Base
|
||||
|
||||
class RefreshToken(Base):
|
||||
__tablename__ = 'refresh_tokens'
|
||||
@@ -1,7 +1,7 @@
|
||||
from sqlalchemy import Column, Integer, String, DateTime
|
||||
from sqlalchemy.orm import relationship
|
||||
from datetime import datetime
|
||||
from ..config.database import Base
|
||||
from ...shared.config.database import Base
|
||||
|
||||
class Role(Base):
|
||||
__tablename__ = 'roles'
|
||||
@@ -1,7 +1,7 @@
|
||||
from sqlalchemy import Column, Integer, String, Boolean, Text, ForeignKey, DateTime, Numeric
|
||||
from sqlalchemy.orm import relationship
|
||||
from datetime import datetime
|
||||
from ..config.database import Base
|
||||
from ...shared.config.database import Base
|
||||
|
||||
class User(Base):
|
||||
__tablename__ = 'users'
|
||||
0
Backend/src/auth/routes/__init__.py
Normal file
0
Backend/src/auth/routes/__init__.py
Normal file
@@ -5,12 +5,12 @@ from pathlib import Path
|
||||
import aiofiles
|
||||
import uuid
|
||||
import os
|
||||
from ..config.database import get_db
|
||||
from ...shared.config.database import get_db
|
||||
from ..services.auth_service import auth_service
|
||||
from ..schemas.auth import RegisterRequest, LoginRequest, RefreshTokenRequest, ForgotPasswordRequest, ResetPasswordRequest, AuthResponse, TokenResponse, MessageResponse, MFAInitResponse, EnableMFARequest, VerifyMFARequest, MFAStatusResponse, UpdateProfileRequest
|
||||
from ..middleware.auth import get_current_user
|
||||
from ...security.middleware.auth import get_current_user
|
||||
from ..models.user import User
|
||||
from ..services.audit_service import audit_service
|
||||
from ...analytics.services.audit_service import audit_service
|
||||
from slowapi import Limiter, _rate_limit_exceeded_handler
|
||||
from slowapi.util import get_remote_address
|
||||
from slowapi.errors import RateLimitExceeded
|
||||
@@ -58,7 +58,7 @@ async def register(
|
||||
|
||||
try:
|
||||
result = await auth_service.register(db=db, name=register_request.name, email=register_request.email, password=register_request.password, phone=register_request.phone)
|
||||
from ..config.settings import settings
|
||||
from ...shared.config.settings import settings
|
||||
max_age = 7 * 24 * 60 * 60 # 7 days for registration
|
||||
# Use secure cookies in production (HTTPS required)
|
||||
# Set access token in httpOnly cookie for security
|
||||
@@ -144,7 +144,7 @@ async def login(
|
||||
status='success'
|
||||
)
|
||||
return {'status': 'success', 'requires_mfa': True, 'user_id': result['user_id']}
|
||||
from ..config.settings import settings
|
||||
from ...shared.config.settings import settings
|
||||
max_age = 7 * 24 * 60 * 60 if login_request.rememberMe else 1 * 24 * 60 * 60
|
||||
# Use secure cookies in production (HTTPS required)
|
||||
# Set access token in httpOnly cookie for security
|
||||
@@ -215,7 +215,7 @@ async def refresh_token(
|
||||
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail='Refresh token not found')
|
||||
try:
|
||||
result = await auth_service.refresh_access_token(db, refreshToken)
|
||||
from ..config.settings import settings
|
||||
from ...shared.config.settings import settings
|
||||
# Set new access token in httpOnly cookie
|
||||
# Use 'lax' in development for cross-origin support, 'strict' in production
|
||||
samesite_value = 'strict' if settings.is_production else 'lax'
|
||||
@@ -250,7 +250,7 @@ async def logout(
|
||||
await auth_service.logout(db, refreshToken)
|
||||
|
||||
# Delete both access and refresh token cookies
|
||||
from ..config.settings import settings
|
||||
from ...shared.config.settings import settings
|
||||
# Use 'lax' in development for cross-origin support, 'strict' in production
|
||||
samesite_value = 'strict' if settings.is_production else 'lax'
|
||||
response.delete_cookie(key='refreshToken', path='/', secure=settings.is_production, samesite=samesite_value)
|
||||
@@ -320,7 +320,7 @@ async def reset_password(request: ResetPasswordRequest, db: Session=Depends(get_
|
||||
status_code = status.HTTP_404_NOT_FOUND
|
||||
raise HTTPException(status_code=status_code, detail=str(e))
|
||||
from ..services.mfa_service import mfa_service
|
||||
from ..config.settings import settings
|
||||
from ...shared.config.settings import settings
|
||||
|
||||
@router.get('/mfa/init')
|
||||
async def init_mfa(current_user: User=Depends(get_current_user), db: Session=Depends(get_db)):
|
||||
@@ -380,7 +380,7 @@ async def regenerate_backup_codes(current_user: User=Depends(get_current_user),
|
||||
async def upload_avatar(request: Request, image: UploadFile=File(...), current_user: User=Depends(get_current_user), db: Session=Depends(get_db)):
|
||||
try:
|
||||
# Use comprehensive file validation (magic bytes + size)
|
||||
from ..utils.file_validation import validate_uploaded_image
|
||||
from ...shared.utils.file_validation import validate_uploaded_image
|
||||
max_avatar_size = 2 * 1024 * 1024 # 2MB for avatars
|
||||
|
||||
# Validate file completely (MIME type, size, magic bytes, integrity)
|
||||
@@ -3,14 +3,14 @@ from sqlalchemy.orm import Session
|
||||
from sqlalchemy import or_
|
||||
from typing import Optional
|
||||
import bcrypt
|
||||
from ..config.database import get_db
|
||||
from ..middleware.auth import get_current_user, authorize_roles
|
||||
from ...shared.config.database import get_db
|
||||
from ...security.middleware.auth import get_current_user, authorize_roles
|
||||
from ..models.user import User
|
||||
from ..models.role import Role
|
||||
from ..models.booking import Booking, BookingStatus
|
||||
from ..utils.role_helpers import can_manage_users
|
||||
from ..utils.response_helpers import success_response
|
||||
from ..services.audit_service import audit_service
|
||||
from ...bookings.models.booking import Booking, BookingStatus
|
||||
from ...shared.utils.role_helpers import can_manage_users
|
||||
from ...shared.utils.response_helpers import success_response
|
||||
from ...analytics.services.audit_service import audit_service
|
||||
from ..schemas.user import CreateUserRequest, UpdateUserRequest
|
||||
router = APIRouter(prefix='/users', tags=['users'])
|
||||
|
||||
0
Backend/src/auth/schemas/__init__.py
Normal file
0
Backend/src/auth/schemas/__init__.py
Normal file
0
Backend/src/auth/services/__init__.py
Normal file
0
Backend/src/auth/services/__init__.py
Normal file
@@ -11,13 +11,13 @@ from ..models.user import User
|
||||
from ..models.refresh_token import RefreshToken
|
||||
from ..models.password_reset_token import PasswordResetToken
|
||||
from ..models.role import Role
|
||||
from ..utils.mailer import send_email
|
||||
from ..utils.email_templates import (
|
||||
from ...shared.utils.mailer import send_email
|
||||
from ...shared.utils.email_templates import (
|
||||
welcome_email_template,
|
||||
password_reset_email_template,
|
||||
password_changed_email_template
|
||||
)
|
||||
from ..config.settings import settings
|
||||
from ...shared.config.settings import settings
|
||||
import os
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -116,7 +116,7 @@ class AuthService:
|
||||
|
||||
async def register(self, db: Session, name: str, email: str, password: str, phone: Optional[str] = None) -> dict:
|
||||
# Validate password strength
|
||||
from ..utils.password_validation import validate_password_strength
|
||||
from ...shared.utils.password_validation import validate_password_strength
|
||||
is_valid, errors = validate_password_strength(password)
|
||||
if not is_valid:
|
||||
error_message = 'Password does not meet requirements: ' + '; '.join(errors)
|
||||
@@ -361,7 +361,7 @@ class AuthService:
|
||||
raise ValueError("Current password is incorrect")
|
||||
|
||||
# Validate new password strength
|
||||
from ..utils.password_validation import validate_password_strength
|
||||
from ...shared.utils.password_validation import validate_password_strength
|
||||
is_valid, errors = validate_password_strength(password)
|
||||
if not is_valid:
|
||||
error_message = 'New password does not meet requirements: ' + '; '.join(errors)
|
||||
@@ -6,9 +6,9 @@ import secrets
|
||||
from urllib.parse import urlencode
|
||||
import logging
|
||||
|
||||
from ..models.security_event import OAuthProvider, OAuthToken
|
||||
from ...security.models.security_event import OAuthProvider, OAuthToken
|
||||
from ..models.user import User
|
||||
from ..config.logging_config import get_logger
|
||||
from ...shared.config.logging_config import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
0
Backend/src/bookings/__init__.py
Normal file
0
Backend/src/bookings/__init__.py
Normal file
0
Backend/src/bookings/models/__init__.py
Normal file
0
Backend/src/bookings/models/__init__.py
Normal file
@@ -2,7 +2,7 @@ from sqlalchemy import Column, Integer, String, DateTime, Numeric, Boolean, Text
|
||||
from sqlalchemy.orm import relationship
|
||||
from datetime import datetime
|
||||
import enum
|
||||
from ..config.database import Base
|
||||
from ...shared.config.database import Base
|
||||
|
||||
class BookingStatus(str, enum.Enum):
|
||||
pending = 'pending'
|
||||
@@ -1,7 +1,7 @@
|
||||
from sqlalchemy import Column, Integer, DateTime, Numeric, Text, ForeignKey
|
||||
from sqlalchemy.orm import relationship
|
||||
from datetime import datetime
|
||||
from ..config.database import Base
|
||||
from ...shared.config.database import Base
|
||||
|
||||
class CheckInCheckOut(Base):
|
||||
__tablename__ = 'checkin_checkout'
|
||||
@@ -2,7 +2,7 @@ from sqlalchemy import Column, Integer, String, DateTime, Numeric, Boolean, Text
|
||||
from sqlalchemy.orm import relationship
|
||||
from datetime import datetime
|
||||
import enum
|
||||
from ..config.database import Base
|
||||
from ...shared.config.database import Base
|
||||
|
||||
class GroupBookingStatus(str, enum.Enum):
|
||||
draft = 'draft'
|
||||
0
Backend/src/bookings/routes/__init__.py
Normal file
0
Backend/src/bookings/routes/__init__.py
Normal file
@@ -5,25 +5,25 @@ from typing import Optional
|
||||
from datetime import datetime
|
||||
import random
|
||||
import os
|
||||
from ..config.database import get_db
|
||||
from ..config.settings import settings
|
||||
from ..middleware.auth import get_current_user, authorize_roles
|
||||
from ..models.user import User
|
||||
from ..models.role import Role
|
||||
from ...shared.config.database import get_db
|
||||
from ...shared.config.settings import settings
|
||||
from ...security.middleware.auth import get_current_user, authorize_roles
|
||||
from ...auth.models.user import User
|
||||
from ...auth.models.role import Role
|
||||
from ..models.booking import Booking, BookingStatus
|
||||
from ..models.room import Room, RoomStatus
|
||||
from ..models.room_type import RoomType
|
||||
from ..models.payment import Payment, PaymentMethod, PaymentType, PaymentStatus
|
||||
from ..models.service_usage import ServiceUsage
|
||||
from ..models.user_loyalty import UserLoyalty
|
||||
from ..models.referral import Referral, ReferralStatus
|
||||
from ..services.room_service import normalize_images, get_base_url
|
||||
from ...rooms.models.room import Room, RoomStatus
|
||||
from ...rooms.models.room_type import RoomType
|
||||
from ...payments.models.payment import Payment, PaymentMethod, PaymentType, PaymentStatus
|
||||
from ...hotel_services.models.service_usage import ServiceUsage
|
||||
from ...loyalty.models.user_loyalty import UserLoyalty
|
||||
from ...loyalty.models.referral import Referral, ReferralStatus
|
||||
from ...rooms.services.room_service import normalize_images, get_base_url
|
||||
from fastapi import Request
|
||||
from ..utils.mailer import send_email
|
||||
from ..utils.email_templates import booking_confirmation_email_template, booking_status_changed_email_template
|
||||
from ..services.loyalty_service import LoyaltyService
|
||||
from ..utils.currency_helpers import get_currency_symbol
|
||||
from ..utils.response_helpers import success_response
|
||||
from ...shared.utils.mailer import send_email
|
||||
from ...shared.utils.email_templates import booking_confirmation_email_template, booking_status_changed_email_template
|
||||
from ...loyalty.services.loyalty_service import LoyaltyService
|
||||
from ...shared.utils.currency_helpers import get_currency_symbol
|
||||
from ...shared.utils.response_helpers import success_response
|
||||
from ..schemas.booking import CreateBookingRequest, UpdateBookingRequest
|
||||
router = APIRouter(prefix='/bookings', tags=['bookings'])
|
||||
|
||||
@@ -216,7 +216,7 @@ async def create_booking(booking_data: CreateBookingRequest, current_user: User=
|
||||
raise HTTPException(status_code=409, detail='Room already booked for the selected dates')
|
||||
|
||||
# Check for maintenance blocks
|
||||
from ..models.room_maintenance import RoomMaintenance, MaintenanceStatus
|
||||
from ...rooms.models.room_maintenance import RoomMaintenance, MaintenanceStatus
|
||||
maintenance_block = db.query(RoomMaintenance).filter(
|
||||
and_(
|
||||
RoomMaintenance.room_id == room_id,
|
||||
@@ -251,7 +251,7 @@ async def create_booking(booking_data: CreateBookingRequest, current_user: User=
|
||||
# Calculate services total if any (using Pydantic model)
|
||||
services_total = 0.0
|
||||
if services:
|
||||
from ..models.service import Service
|
||||
from ...hotel_services.models.service import Service
|
||||
for service_item in services:
|
||||
service_id = service_item.service_id
|
||||
quantity = service_item.quantity
|
||||
@@ -314,8 +314,8 @@ async def create_booking(booking_data: CreateBookingRequest, current_user: User=
|
||||
# Process referral code if provided
|
||||
if referral_code:
|
||||
try:
|
||||
from ..services.loyalty_service import LoyaltyService
|
||||
from ..models.system_settings import SystemSettings
|
||||
from ...loyalty.services.loyalty_service import LoyaltyService
|
||||
from ...system.models.system_settings import SystemSettings
|
||||
|
||||
# Check if loyalty program is enabled
|
||||
setting = db.query(SystemSettings).filter(
|
||||
@@ -338,7 +338,7 @@ async def create_booking(booking_data: CreateBookingRequest, current_user: User=
|
||||
logger.warning(f"Failed to process referral code {referral_code}: {referral_error}")
|
||||
# Don't fail the booking if referral processing fails
|
||||
if payment_method in ['stripe', 'paypal']:
|
||||
from ..models.payment import Payment, PaymentMethod, PaymentStatus, PaymentType
|
||||
from ...payments.models.payment import Payment, PaymentMethod, PaymentStatus, PaymentType
|
||||
if payment_method == 'stripe':
|
||||
payment_method_enum = PaymentMethod.stripe
|
||||
elif payment_method == 'paypal':
|
||||
@@ -352,14 +352,14 @@ async def create_booking(booking_data: CreateBookingRequest, current_user: User=
|
||||
db.flush()
|
||||
logger.info(f'Payment created: ID={payment.id}, method={(payment.payment_method.value if hasattr(payment.payment_method, 'value') else payment.payment_method)}')
|
||||
if requires_deposit and deposit_amount > 0:
|
||||
from ..models.payment import Payment, PaymentMethod, PaymentStatus, PaymentType
|
||||
from ...payments.models.payment import Payment, PaymentMethod, PaymentStatus, PaymentType
|
||||
deposit_payment = Payment(booking_id=booking.id, amount=deposit_amount, payment_method=PaymentMethod.stripe, payment_type=PaymentType.deposit, deposit_percentage=deposit_percentage, payment_status=PaymentStatus.pending, payment_date=None)
|
||||
db.add(deposit_payment)
|
||||
db.flush()
|
||||
logger.info(f'Deposit payment created: ID={deposit_payment.id}, amount={deposit_amount}, percentage={deposit_percentage}%')
|
||||
# Add service usages (services already extracted from Pydantic model)
|
||||
if services:
|
||||
from ..models.service import Service
|
||||
from ...hotel_services.models.service import Service
|
||||
for service_item in services:
|
||||
service_id = service_item.service_id
|
||||
quantity = service_item.quantity
|
||||
@@ -375,7 +375,7 @@ async def create_booking(booking_data: CreateBookingRequest, current_user: User=
|
||||
|
||||
# Send booking confirmation notification
|
||||
try:
|
||||
from ..services.notification_service import NotificationService
|
||||
from ...notifications.services.notification_service import NotificationService
|
||||
if booking.status == BookingStatus.confirmed:
|
||||
NotificationService.send_booking_confirmation(db, booking)
|
||||
except Exception as e:
|
||||
@@ -384,11 +384,11 @@ async def create_booking(booking_data: CreateBookingRequest, current_user: User=
|
||||
logger.warning(f'Failed to send booking confirmation notification: {e}')
|
||||
|
||||
try:
|
||||
from ..services.invoice_service import InvoiceService
|
||||
from ..utils.mailer import send_email
|
||||
from ...payments.services.invoice_service import InvoiceService
|
||||
from ...shared.utils.mailer import send_email
|
||||
from sqlalchemy.orm import joinedload, selectinload
|
||||
booking = db.query(Booking).options(selectinload(Booking.service_usages).selectinload(ServiceUsage.service)).filter(Booking.id == booking.id).first()
|
||||
from ..models.system_settings import SystemSettings
|
||||
from ...system.models.system_settings import SystemSettings
|
||||
company_settings = {}
|
||||
for key in ['company_name', 'company_address', 'company_phone', 'company_email', 'company_tax_id', 'company_logo_url']:
|
||||
setting = db.query(SystemSettings).filter(SystemSettings.key == key).first()
|
||||
@@ -503,7 +503,7 @@ async def get_booking_by_id(id: int, request: Request, current_user: User=Depend
|
||||
booking = db.query(Booking).options(selectinload(Booking.payments), selectinload(Booking.service_usages).selectinload(ServiceUsage.service), joinedload(Booking.user), joinedload(Booking.room).joinedload(Room.room_type)).filter(Booking.id == id).first()
|
||||
if not booking:
|
||||
raise HTTPException(status_code=404, detail='Booking not found')
|
||||
from ..utils.role_helpers import is_admin
|
||||
from ...shared.utils.role_helpers import is_admin
|
||||
if not is_admin(current_user, db) and booking.user_id != current_user.id:
|
||||
raise HTTPException(status_code=403, detail='Forbidden')
|
||||
import logging
|
||||
@@ -612,7 +612,7 @@ async def cancel_booking(id: int, current_user: User=Depends(get_current_user),
|
||||
|
||||
if not active_booking:
|
||||
# Check for maintenance
|
||||
from ..models.room_maintenance import RoomMaintenance, MaintenanceStatus
|
||||
from ...rooms.models.room_maintenance import RoomMaintenance, MaintenanceStatus
|
||||
active_maintenance = db.query(RoomMaintenance).filter(
|
||||
and_(
|
||||
RoomMaintenance.room_id == booking.room_id,
|
||||
@@ -630,7 +630,7 @@ async def cancel_booking(id: int, current_user: User=Depends(get_current_user),
|
||||
db.flush()
|
||||
db.commit()
|
||||
try:
|
||||
from ..models.system_settings import SystemSettings
|
||||
from ...system.models.system_settings import SystemSettings
|
||||
client_url_setting = db.query(SystemSettings).filter(SystemSettings.key == 'client_url').first()
|
||||
client_url = client_url_setting.value if client_url_setting and client_url_setting.value else settings.CLIENT_URL or os.getenv('CLIENT_URL', 'http://localhost:5173')
|
||||
email_html = booking_status_changed_email_template(booking_number=booking.booking_number, guest_name=booking.user.full_name if booking.user else 'Guest', status='cancelled', client_url=client_url)
|
||||
@@ -673,7 +673,7 @@ async def update_booking(id: int, booking_data: UpdateBookingRequest, current_us
|
||||
# Set room to cleaning when checked out (housekeeping needed)
|
||||
if room:
|
||||
# Check if there's active maintenance
|
||||
from ..models.room_maintenance import RoomMaintenance, MaintenanceStatus
|
||||
from ...rooms.models.room_maintenance import RoomMaintenance, MaintenanceStatus
|
||||
active_maintenance = db.query(RoomMaintenance).filter(
|
||||
and_(
|
||||
RoomMaintenance.room_id == room.id,
|
||||
@@ -710,7 +710,7 @@ async def update_booking(id: int, booking_data: UpdateBookingRequest, current_us
|
||||
|
||||
if not active_booking:
|
||||
# Check for maintenance
|
||||
from ..models.room_maintenance import RoomMaintenance, MaintenanceStatus
|
||||
from ...rooms.models.room_maintenance import RoomMaintenance, MaintenanceStatus
|
||||
active_maintenance = db.query(RoomMaintenance).filter(
|
||||
and_(
|
||||
RoomMaintenance.room_id == room.id,
|
||||
@@ -753,7 +753,7 @@ async def update_booking(id: int, booking_data: UpdateBookingRequest, current_us
|
||||
# Send booking confirmation notification if status changed to confirmed
|
||||
if new_status == BookingStatus.confirmed:
|
||||
try:
|
||||
from ..services.notification_service import NotificationService
|
||||
from ...notifications.services.notification_service import NotificationService
|
||||
NotificationService.send_booking_confirmation(db, booking)
|
||||
except Exception as e:
|
||||
import logging
|
||||
@@ -778,7 +778,7 @@ async def update_booking(id: int, booking_data: UpdateBookingRequest, current_us
|
||||
if status_value and old_status != booking.status:
|
||||
if booking.status in [BookingStatus.confirmed, BookingStatus.cancelled]:
|
||||
try:
|
||||
from ..models.system_settings import SystemSettings
|
||||
from ...system.models.system_settings import SystemSettings
|
||||
client_url_setting = db.query(SystemSettings).filter(SystemSettings.key == 'client_url').first()
|
||||
client_url = client_url_setting.value if client_url_setting and client_url_setting.value else settings.CLIENT_URL or os.getenv('CLIENT_URL', 'http://localhost:5173')
|
||||
if booking.status == BookingStatus.confirmed:
|
||||
@@ -797,7 +797,7 @@ async def update_booking(id: int, booking_data: UpdateBookingRequest, current_us
|
||||
if booking.user:
|
||||
try:
|
||||
# Check if booking already earned points
|
||||
from ..models.loyalty_point_transaction import LoyaltyPointTransaction, TransactionSource
|
||||
from ...loyalty.models.loyalty_point_transaction import LoyaltyPointTransaction, TransactionSource
|
||||
existing_points = db.query(LoyaltyPointTransaction).filter(
|
||||
LoyaltyPointTransaction.booking_id == booking.id,
|
||||
LoyaltyPointTransaction.source == TransactionSource.booking
|
||||
@@ -815,7 +815,7 @@ async def update_booking(id: int, booking_data: UpdateBookingRequest, current_us
|
||||
user_loyalty = db.query(UserLoyalty).filter(UserLoyalty.user_id == booking.user_id).first()
|
||||
if user_loyalty and user_loyalty.referral_code:
|
||||
# Check if there's a referral for this user that hasn't been rewarded yet
|
||||
from ..models.referral import Referral
|
||||
from ...loyalty.models.referral import Referral
|
||||
referral = db.query(Referral).filter(
|
||||
Referral.referred_user_id == booking.user_id,
|
||||
Referral.booking_id == booking.id,
|
||||
@@ -999,7 +999,7 @@ async def admin_create_booking(booking_data: dict, current_user: User=Depends(au
|
||||
services = booking_data.get('services', [])
|
||||
services_total = 0.0
|
||||
if services:
|
||||
from ..models.service import Service
|
||||
from ...hotel_services.models.service import Service
|
||||
for service_item in services:
|
||||
service_id = service_item.get('service_id')
|
||||
quantity = service_item.get('quantity', 1)
|
||||
@@ -1078,7 +1078,7 @@ async def admin_create_booking(booking_data: dict, current_user: User=Depends(au
|
||||
db.flush()
|
||||
|
||||
# Create payment records based on payment_status
|
||||
from ..models.payment import Payment, PaymentMethod, PaymentStatus, PaymentType
|
||||
from ...payments.models.payment import Payment, PaymentMethod, PaymentStatus, PaymentType
|
||||
from datetime import datetime as dt
|
||||
|
||||
# Determine payment method enum
|
||||
@@ -1179,7 +1179,7 @@ async def admin_create_booking(booking_data: dict, current_user: User=Depends(au
|
||||
|
||||
# Add service usages if any
|
||||
if services:
|
||||
from ..models.service import Service
|
||||
from ...hotel_services.models.service import Service
|
||||
for service_item in services:
|
||||
service_id = service_item.get('service_id')
|
||||
quantity = service_item.get('quantity', 1)
|
||||
@@ -4,18 +4,18 @@ from typing import Optional, List
|
||||
from datetime import datetime
|
||||
from decimal import Decimal
|
||||
|
||||
from ..config.database import get_db
|
||||
from ..middleware.auth import get_current_user, authorize_roles
|
||||
from ..models.user import User
|
||||
from ..models.role import Role
|
||||
from ...shared.config.database import get_db
|
||||
from ...security.middleware.auth import get_current_user, authorize_roles
|
||||
from ...auth.models.user import User
|
||||
from ...auth.models.role import Role
|
||||
from ..models.group_booking import (
|
||||
GroupBooking, GroupBookingMember, GroupRoomBlock, GroupPayment,
|
||||
GroupBookingStatus, PaymentOption
|
||||
)
|
||||
from ..models.room import Room
|
||||
from ..models.room_type import RoomType
|
||||
from ...rooms.models.room import Room
|
||||
from ...rooms.models.room_type import RoomType
|
||||
from ..services.group_booking_service import GroupBookingService
|
||||
from ..services.room_service import get_base_url
|
||||
from ...rooms.services.room_service import get_base_url
|
||||
from fastapi import Request
|
||||
|
||||
router = APIRouter(prefix='/group-bookings', tags=['group-bookings'])
|
||||
0
Backend/src/bookings/schemas/__init__.py
Normal file
0
Backend/src/bookings/schemas/__init__.py
Normal file
0
Backend/src/bookings/services/__init__.py
Normal file
0
Backend/src/bookings/services/__init__.py
Normal file
@@ -9,10 +9,10 @@ from ..models.group_booking import (
|
||||
GroupBookingStatus, PaymentOption
|
||||
)
|
||||
from ..models.booking import Booking, BookingStatus
|
||||
from ..models.room import Room, RoomStatus
|
||||
from ..models.room_type import RoomType
|
||||
from ..models.user import User
|
||||
from ..models.payment import Payment, PaymentStatus, PaymentMethod
|
||||
from ...rooms.models.room import Room, RoomStatus
|
||||
from ...rooms.models.room_type import RoomType
|
||||
from ...auth.models.user import User
|
||||
from ...payments.models.payment import Payment, PaymentStatus, PaymentMethod
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
0
Backend/src/content/__init__.py
Normal file
0
Backend/src/content/__init__.py
Normal file
0
Backend/src/content/models/__init__.py
Normal file
0
Backend/src/content/models/__init__.py
Normal file
@@ -1,7 +1,7 @@
|
||||
from sqlalchemy import Column, Integer, String, DateTime, Boolean, Text
|
||||
from sqlalchemy.orm import relationship
|
||||
from datetime import datetime
|
||||
from ..config.database import Base
|
||||
from ...shared.config.database import Base
|
||||
|
||||
class Banner(Base):
|
||||
__tablename__ = 'banners'
|
||||
@@ -1,7 +1,7 @@
|
||||
from sqlalchemy import Column, Integer, String, Text, DateTime, Boolean, ForeignKey, JSON
|
||||
from sqlalchemy.orm import relationship
|
||||
from datetime import datetime
|
||||
from ..config.database import Base
|
||||
from ...shared.config.database import Base
|
||||
|
||||
class BlogPost(Base):
|
||||
__tablename__ = 'blog_posts'
|
||||
@@ -1,7 +1,7 @@
|
||||
from datetime import datetime
|
||||
from sqlalchemy import Column, DateTime, ForeignKey, Integer, String
|
||||
from sqlalchemy.orm import relationship
|
||||
from ..config.database import Base
|
||||
from ...shared.config.database import Base
|
||||
|
||||
class CookieIntegrationConfig(Base):
|
||||
__tablename__ = 'cookie_integration_configs'
|
||||
@@ -1,7 +1,7 @@
|
||||
from datetime import datetime
|
||||
from sqlalchemy import Boolean, Column, DateTime, ForeignKey, Integer
|
||||
from sqlalchemy.orm import relationship
|
||||
from ..config.database import Base
|
||||
from ...shared.config.database import Base
|
||||
|
||||
class CookiePolicy(Base):
|
||||
__tablename__ = 'cookie_policies'
|
||||
@@ -2,7 +2,7 @@ from sqlalchemy import Column, Integer, String, Text, DateTime, Boolean, Enum as
|
||||
from sqlalchemy.orm import relationship
|
||||
from datetime import datetime
|
||||
import enum
|
||||
from ..config.database import Base
|
||||
from ...shared.config.database import Base
|
||||
|
||||
class PageType(str, enum.Enum):
|
||||
HOME = 'home'
|
||||
0
Backend/src/content/routes/__init__.py
Normal file
0
Backend/src/content/routes/__init__.py
Normal file
@@ -1,8 +1,8 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlalchemy.orm import Session
|
||||
import json
|
||||
from ..config.database import get_db
|
||||
from ..config.logging_config import get_logger
|
||||
from ...shared.config.database import get_db
|
||||
from ...shared.config.logging_config import get_logger
|
||||
from ..models.page_content import PageContent, PageType
|
||||
logger = get_logger(__name__)
|
||||
router = APIRouter(prefix='/about', tags=['about'])
|
||||
@@ -1,7 +1,7 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlalchemy.orm import Session
|
||||
from ..config.database import get_db
|
||||
from ..config.logging_config import get_logger
|
||||
from ...shared.config.database import get_db
|
||||
from ...shared.config.logging_config import get_logger
|
||||
from ..models.page_content import PageContent, PageType
|
||||
logger = get_logger(__name__)
|
||||
router = APIRouter(prefix='/accessibility', tags=['accessibility'])
|
||||
@@ -1,10 +1,10 @@
|
||||
from fastapi import APIRouter, Depends, status
|
||||
from sqlalchemy.orm import Session
|
||||
from ..config.database import get_db
|
||||
from ..middleware.auth import authorize_roles
|
||||
from ..models.user import User
|
||||
from ...shared.config.database import get_db
|
||||
from ...security.middleware.auth import authorize_roles
|
||||
from ...auth.models.user import User
|
||||
from ..schemas.admin_privacy import CookieIntegrationSettings, CookieIntegrationSettingsResponse, CookiePolicySettings, CookiePolicySettingsResponse
|
||||
from ..services.privacy_admin_service import privacy_admin_service
|
||||
from ...security.services.privacy_admin_service import privacy_admin_service
|
||||
router = APIRouter(prefix='/admin/privacy', tags=['admin-privacy'])
|
||||
|
||||
@router.get('/cookie-policy', response_model=CookiePolicySettingsResponse, status_code=status.HTTP_200_OK)
|
||||
@@ -7,9 +7,9 @@ from pathlib import Path
|
||||
import os
|
||||
import aiofiles
|
||||
import uuid
|
||||
from ..config.database import get_db
|
||||
from ..middleware.auth import get_current_user, authorize_roles
|
||||
from ..models.user import User
|
||||
from ...shared.config.database import get_db
|
||||
from ...security.middleware.auth import get_current_user, authorize_roles
|
||||
from ...auth.models.user import User
|
||||
from ..models.banner import Banner
|
||||
router = APIRouter(prefix='/banners', tags=['banners'])
|
||||
|
||||
@@ -136,8 +136,8 @@ async def upload_banner_image(request: Request, image: UploadFile=File(...), cur
|
||||
filename = f'banner-{uuid.uuid4()}{ext}'
|
||||
file_path = upload_dir / filename
|
||||
# Use comprehensive file validation (magic bytes + size)
|
||||
from ..config.settings import settings
|
||||
from ..utils.file_validation import validate_uploaded_image
|
||||
from ...shared.config.settings import settings
|
||||
from ...shared.utils.file_validation import validate_uploaded_image
|
||||
max_size = settings.MAX_UPLOAD_SIZE
|
||||
|
||||
# Validate file completely (MIME type, size, magic bytes, integrity)
|
||||
@@ -9,13 +9,13 @@ import os
|
||||
import uuid
|
||||
import aiofiles
|
||||
from pathlib import Path
|
||||
from ..config.database import get_db
|
||||
from ..config.logging_config import get_logger
|
||||
from ..middleware.auth import get_current_user, get_current_user_optional, authorize_roles
|
||||
from ..models.user import User
|
||||
from ...shared.config.database import get_db
|
||||
from ...shared.config.logging_config import get_logger
|
||||
from ...security.middleware.auth import get_current_user, get_current_user_optional, authorize_roles
|
||||
from ...auth.models.user import User
|
||||
from ..models.blog import BlogPost
|
||||
from ..schemas.blog import BlogPostCreate, BlogPostUpdate, BlogPostResponse, BlogPostListResponse
|
||||
from ..utils.response_helpers import success_response
|
||||
from ...shared.utils.response_helpers import success_response
|
||||
|
||||
logger = get_logger(__name__)
|
||||
router = APIRouter(prefix='/blog', tags=['blog'])
|
||||
@@ -153,7 +153,7 @@ async def get_blog_post_by_slug(
|
||||
|
||||
# Only show published posts to non-admin users
|
||||
# Check if user is admin
|
||||
from ..models.role import Role
|
||||
from ...auth.models.role import Role
|
||||
is_admin = False
|
||||
if current_user:
|
||||
role = db.query(Role).filter(Role.id == current_user.role_id).first()
|
||||
@@ -540,8 +540,8 @@ async def upload_blog_image(
|
||||
file_path = upload_dir / filename
|
||||
|
||||
# Validate file
|
||||
from ..config.settings import settings
|
||||
from ..utils.file_validation import validate_uploaded_image
|
||||
from ...shared.config.settings import settings
|
||||
from ...shared.utils.file_validation import validate_uploaded_image
|
||||
max_size = settings.MAX_UPLOAD_SIZE
|
||||
|
||||
content = await validate_uploaded_image(image, max_size)
|
||||
@@ -1,7 +1,7 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlalchemy.orm import Session
|
||||
from ..config.database import get_db
|
||||
from ..config.logging_config import get_logger
|
||||
from ...shared.config.database import get_db
|
||||
from ...shared.config.logging_config import get_logger
|
||||
from ..models.page_content import PageContent, PageType
|
||||
logger = get_logger(__name__)
|
||||
router = APIRouter(prefix='/cancellation', tags=['cancellation'])
|
||||
@@ -1,8 +1,8 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlalchemy.orm import Session
|
||||
import json
|
||||
from ..config.database import get_db
|
||||
from ..config.logging_config import get_logger
|
||||
from ...shared.config.database import get_db
|
||||
from ...shared.config.logging_config import get_logger
|
||||
from ..models.page_content import PageContent, PageType
|
||||
logger = get_logger(__name__)
|
||||
router = APIRouter(prefix='/contact-content', tags=['contact-content'])
|
||||
@@ -3,12 +3,12 @@ from sqlalchemy.orm import Session
|
||||
from pydantic import BaseModel, EmailStr
|
||||
from typing import Optional
|
||||
import logging
|
||||
from ..config.database import get_db
|
||||
from ..models.user import User
|
||||
from ..models.role import Role
|
||||
from ..models.system_settings import SystemSettings
|
||||
from ..utils.mailer import send_email
|
||||
from ..utils.html_sanitizer import sanitize_text_for_html
|
||||
from ...shared.config.database import get_db
|
||||
from ...auth.models.user import User
|
||||
from ...auth.models.role import Role
|
||||
from ...system.models.system_settings import SystemSettings
|
||||
from ...shared.utils.mailer import send_email
|
||||
from ...shared.utils.html_sanitizer import sanitize_text_for_html
|
||||
logger = logging.getLogger(__name__)
|
||||
router = APIRouter(prefix='/contact', tags=['contact'])
|
||||
|
||||
@@ -31,7 +31,7 @@ def get_admin_email(db: Session) -> str:
|
||||
admin_user = db.query(User).filter(User.role_id == admin_role.id, User.is_active == True).first()
|
||||
if admin_user:
|
||||
return admin_user.email
|
||||
from ..config.settings import settings
|
||||
from ...shared.config.settings import settings
|
||||
if settings.SMTP_FROM_EMAIL:
|
||||
return settings.SMTP_FROM_EMAIL
|
||||
raise HTTPException(status_code=500, detail='Admin email not configured. Please set company_email in system settings or ensure an admin user exists.')
|
||||
@@ -1,7 +1,7 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlalchemy.orm import Session
|
||||
from ..config.database import get_db
|
||||
from ..config.logging_config import get_logger
|
||||
from ...shared.config.database import get_db
|
||||
from ...shared.config.logging_config import get_logger
|
||||
from ..models.page_content import PageContent, PageType
|
||||
logger = get_logger(__name__)
|
||||
router = APIRouter(prefix='/faq', tags=['faq'])
|
||||
@@ -1,8 +1,8 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlalchemy.orm import Session
|
||||
import json
|
||||
from ..config.database import get_db
|
||||
from ..config.logging_config import get_logger
|
||||
from ...shared.config.database import get_db
|
||||
from ...shared.config.logging_config import get_logger
|
||||
from ..models.page_content import PageContent, PageType
|
||||
logger = get_logger(__name__)
|
||||
router = APIRouter(prefix='/footer', tags=['footer'])
|
||||
@@ -1,8 +1,8 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlalchemy.orm import Session
|
||||
import json
|
||||
from ..config.database import get_db
|
||||
from ..config.logging_config import get_logger
|
||||
from ...shared.config.database import get_db
|
||||
from ...shared.config.logging_config import get_logger
|
||||
from ..models.page_content import PageContent, PageType
|
||||
logger = get_logger(__name__)
|
||||
router = APIRouter(prefix='/home', tags=['home'])
|
||||
@@ -7,13 +7,13 @@ import json
|
||||
import os
|
||||
import aiofiles
|
||||
import uuid
|
||||
from ..config.database import get_db
|
||||
from ..config.logging_config import get_logger
|
||||
from ..middleware.auth import get_current_user, authorize_roles
|
||||
from ..models.user import User
|
||||
from ...shared.config.database import get_db
|
||||
from ...shared.config.logging_config import get_logger
|
||||
from ...security.middleware.auth import get_current_user, authorize_roles
|
||||
from ...auth.models.user import User
|
||||
from ..models.page_content import PageContent, PageType
|
||||
from ..schemas.page_content import PageContentUpdateRequest
|
||||
from ..utils.html_sanitizer import sanitize_html
|
||||
from ...shared.utils.html_sanitizer import sanitize_html
|
||||
logger = get_logger(__name__)
|
||||
router = APIRouter(prefix='/page-content', tags=['page-content'])
|
||||
|
||||
@@ -63,8 +63,8 @@ async def upload_page_content_image(request: Request, image: UploadFile=File(...
|
||||
filename = f'page-content-{uuid.uuid4()}{ext}'
|
||||
file_path = upload_dir / filename
|
||||
# Use comprehensive file validation (magic bytes + size)
|
||||
from ..config.settings import settings
|
||||
from ..utils.file_validation import validate_uploaded_image
|
||||
from ...shared.config.settings import settings
|
||||
from ...shared.utils.file_validation import validate_uploaded_image
|
||||
max_size = settings.MAX_UPLOAD_SIZE
|
||||
|
||||
# Validate file completely (MIME type, size, magic bytes, integrity)
|
||||
@@ -2,10 +2,10 @@ from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlalchemy.orm import Session
|
||||
import json
|
||||
from datetime import datetime
|
||||
from ..config.database import get_db
|
||||
from ..config.logging_config import get_logger
|
||||
from ...shared.config.database import get_db
|
||||
from ...shared.config.logging_config import get_logger
|
||||
from ..models.page_content import PageContent, PageType
|
||||
from ..services.privacy_admin_service import privacy_admin_service
|
||||
from ...security.services.privacy_admin_service import privacy_admin_service
|
||||
from ..schemas.privacy import CookieConsent, CookieConsentResponse, UpdateCookieConsentRequest, CookieCategoryPreferences
|
||||
from ..schemas.admin_privacy import PublicPrivacyConfigResponse
|
||||
logger = get_logger(__name__)
|
||||
@@ -1,8 +1,8 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlalchemy.orm import Session
|
||||
import json
|
||||
from ..config.database import get_db
|
||||
from ..config.logging_config import get_logger
|
||||
from ...shared.config.database import get_db
|
||||
from ...shared.config.logging_config import get_logger
|
||||
from ..models.page_content import PageContent, PageType
|
||||
logger = get_logger(__name__)
|
||||
router = APIRouter(prefix='/refunds', tags=['refunds'])
|
||||
@@ -1,8 +1,8 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlalchemy.orm import Session
|
||||
import json
|
||||
from ..config.database import get_db
|
||||
from ..config.logging_config import get_logger
|
||||
from ...shared.config.database import get_db
|
||||
from ...shared.config.logging_config import get_logger
|
||||
from ..models.page_content import PageContent, PageType
|
||||
logger = get_logger(__name__)
|
||||
router = APIRouter(prefix='/terms', tags=['terms'])
|
||||
0
Backend/src/content/schemas/__init__.py
Normal file
0
Backend/src/content/schemas/__init__.py
Normal file
0
Backend/src/content/services/__init__.py
Normal file
0
Backend/src/content/services/__init__.py
Normal file
0
Backend/src/guest_management/__init__.py
Normal file
0
Backend/src/guest_management/__init__.py
Normal file
0
Backend/src/guest_management/models/__init__.py
Normal file
0
Backend/src/guest_management/models/__init__.py
Normal file
@@ -2,7 +2,7 @@ from sqlalchemy import Column, Integer, String, Text, ForeignKey, DateTime, Enum
|
||||
from sqlalchemy.orm import relationship
|
||||
from datetime import datetime
|
||||
import enum
|
||||
from ..config.database import Base
|
||||
from ...shared.config.database import Base
|
||||
|
||||
class CommunicationType(str, enum.Enum):
|
||||
email = 'email'
|
||||
@@ -1,7 +1,7 @@
|
||||
from sqlalchemy import Column, Integer, String, Text, ForeignKey, DateTime, Boolean
|
||||
from sqlalchemy.orm import relationship
|
||||
from datetime import datetime
|
||||
from ..config.database import Base
|
||||
from ...shared.config.database import Base
|
||||
|
||||
class GuestNote(Base):
|
||||
__tablename__ = 'guest_notes'
|
||||
@@ -1,7 +1,7 @@
|
||||
from sqlalchemy import Column, Integer, String, Text, JSON, ForeignKey, DateTime, Boolean
|
||||
from sqlalchemy.orm import relationship
|
||||
from datetime import datetime
|
||||
from ..config.database import Base
|
||||
from ...shared.config.database import Base
|
||||
|
||||
class GuestPreference(Base):
|
||||
__tablename__ = 'guest_preferences'
|
||||
@@ -1,7 +1,7 @@
|
||||
from sqlalchemy import Column, Integer, String, Text, ForeignKey, DateTime, Boolean, JSON, Table
|
||||
from sqlalchemy.orm import relationship
|
||||
from datetime import datetime
|
||||
from ..config.database import Base
|
||||
from ...shared.config.database import Base
|
||||
|
||||
# Association table for many-to-many relationship between users and segments
|
||||
guest_segment_association = Table(
|
||||
@@ -1,7 +1,7 @@
|
||||
from sqlalchemy import Column, Integer, String, ForeignKey, DateTime, Table
|
||||
from sqlalchemy.orm import relationship
|
||||
from datetime import datetime
|
||||
from ..config.database import Base
|
||||
from ...shared.config.database import Base
|
||||
|
||||
# Association table for many-to-many relationship between users and tags
|
||||
guest_tag_association = Table(
|
||||
0
Backend/src/guest_management/routes/__init__.py
Normal file
0
Backend/src/guest_management/routes/__init__.py
Normal file
@@ -1,16 +1,16 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from sqlalchemy.orm import Session
|
||||
from typing import Optional, List
|
||||
from ..config.database import get_db
|
||||
from ..middleware.auth import get_current_user, authorize_roles
|
||||
from ..models.user import User
|
||||
from ...shared.config.database import get_db
|
||||
from ...security.middleware.auth import get_current_user, authorize_roles
|
||||
from ...auth.models.user import User
|
||||
from ..models.guest_preference import GuestPreference
|
||||
from ..models.guest_note import GuestNote
|
||||
from ..models.guest_tag import GuestTag
|
||||
from ..models.guest_communication import GuestCommunication, CommunicationType, CommunicationDirection
|
||||
from ..models.guest_segment import GuestSegment
|
||||
from ..services.guest_profile_service import GuestProfileService
|
||||
from ..utils.role_helpers import is_customer
|
||||
from ...shared.utils.role_helpers import is_customer
|
||||
import json
|
||||
|
||||
router = APIRouter(prefix='/guest-profiles', tags=['guest-profiles'])
|
||||
@@ -90,7 +90,7 @@ async def get_guest_profile(
|
||||
raise HTTPException(status_code=404, detail=f'User with ID {user_id} not found')
|
||||
|
||||
# Check if user is a customer
|
||||
from ..utils.role_helpers import is_customer
|
||||
from ...shared.utils.role_helpers import is_customer
|
||||
if not is_customer(user, db):
|
||||
raise HTTPException(status_code=404, detail=f'User with ID {user_id} is not a guest (customer)')
|
||||
|
||||
0
Backend/src/guest_management/schemas/__init__.py
Normal file
0
Backend/src/guest_management/schemas/__init__.py
Normal file
0
Backend/src/guest_management/services/__init__.py
Normal file
0
Backend/src/guest_management/services/__init__.py
Normal file
@@ -3,10 +3,10 @@ from sqlalchemy import func, and_, or_, desc
|
||||
from typing import List, Dict, Optional
|
||||
from datetime import datetime, timedelta
|
||||
from decimal import Decimal
|
||||
from ..models.user import User
|
||||
from ..models.booking import Booking, BookingStatus
|
||||
from ..models.payment import Payment
|
||||
from ..models.review import Review
|
||||
from ...auth.models.user import User
|
||||
from ...bookings.models.booking import Booking, BookingStatus
|
||||
from ...payments.models.payment import Payment
|
||||
from ...reviews.models.review import Review
|
||||
from ..models.guest_preference import GuestPreference
|
||||
from ..models.guest_note import GuestNote
|
||||
from ..models.guest_tag import GuestTag
|
||||
@@ -18,7 +18,7 @@ class GuestProfileService:
|
||||
@staticmethod
|
||||
def calculate_lifetime_value(user_id: int, db: Session) -> Decimal:
|
||||
"""Calculate guest lifetime value from all bookings and payments"""
|
||||
from ..models.payment import PaymentStatus
|
||||
from ...payments.models.payment import PaymentStatus
|
||||
|
||||
# Get payments through bookings
|
||||
total_revenue = db.query(func.coalesce(func.sum(Payment.amount), 0)).join(
|
||||
@@ -29,7 +29,7 @@ class GuestProfileService:
|
||||
).scalar()
|
||||
|
||||
# Also include service bookings
|
||||
from ..models.service_booking import ServiceBooking, ServiceBookingStatus
|
||||
from ...hotel_services.models.service_booking import ServiceBooking, ServiceBookingStatus
|
||||
service_revenue = db.query(func.coalesce(func.sum(ServiceBooking.total_amount), 0)).filter(
|
||||
ServiceBooking.user_id == user_id,
|
||||
ServiceBooking.status == ServiceBookingStatus.completed
|
||||
0
Backend/src/hotel_services/__init__.py
Normal file
0
Backend/src/hotel_services/__init__.py
Normal file
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user