This commit is contained in:
Iliyan Angelov
2025-11-30 22:43:09 +02:00
parent 24b40450dd
commit 39fcfff811
1610 changed files with 5442 additions and 1383 deletions

View File

@@ -8,8 +8,8 @@ from pathlib import Path
from dotenv import load_dotenv from dotenv import load_dotenv
load_dotenv() load_dotenv()
sys.path.insert(0, str(Path(__file__).resolve().parents[1])) sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
from src.config.database import Base from src.shared.config.database import Base
from src.config.settings import settings from src.shared.config.settings import settings
from src.models import * from src.models import *
config = context.config config = context.config
if config.config_file_name is not None: if config.config_file_name is not None:

View File

@@ -1,6 +1,6 @@
import uvicorn import uvicorn
from src.config.settings import settings from src.shared.config.settings import settings
from src.config.logging_config import setup_logging, get_logger from src.shared.config.logging_config import setup_logging, get_logger
setup_logging() setup_logging()
logger = get_logger(__name__) logger = get_logger(__name__)
if __name__ == '__main__': if __name__ == '__main__':

View File

@@ -1,16 +0,0 @@
#!/bin/bash
# Script to run integration tests for the Hotel Booking API
echo "Running integration tests for Hotel Booking API..."
echo "=================================================="
# Change to Backend directory
cd "$(dirname "$0")"
# Run pytest with integration marker
pytest src/tests/ -v -m integration --tb=short
# Exit with pytest's exit code
exit $?

View File

@@ -11,7 +11,7 @@ from pathlib import Path
backend_dir = Path(__file__).parent backend_dir = Path(__file__).parent
sys.path.insert(0, str(backend_dir)) sys.path.insert(0, str(backend_dir))
from src.config.database import SessionLocal from src.shared.config.database import SessionLocal
from src.models.role import Role from src.models.role import Role
def add_accountant_role(): def add_accountant_role():

View File

@@ -6,7 +6,7 @@ from pathlib import Path
sys.path.insert(0, str(Path(__file__).parent.parent)) sys.path.insert(0, str(Path(__file__).parent.parent))
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from src.config.database import SessionLocal from src.shared.config.database import SessionLocal
from src.models.blog import BlogPost from src.models.blog import BlogPost
from datetime import datetime, timedelta from datetime import datetime, timedelta

View File

@@ -8,7 +8,7 @@ import json
sys.path.insert(0, str(Path(__file__).parent)) sys.path.insert(0, str(Path(__file__).parent))
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from src.config.database import SessionLocal from src.shared.config.database import SessionLocal
from src.models.page_content import PageContent, PageType from src.models.page_content import PageContent, PageType
from datetime import datetime from datetime import datetime

View File

@@ -3,7 +3,7 @@ import os
from datetime import datetime, timedelta from datetime import datetime, timedelta
sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'src')) sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'src'))
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from src.config.database import SessionLocal from src.shared.config.database import SessionLocal
from src.models.banner import Banner from src.models.banner import Banner
from src.models.system_settings import SystemSettings from src.models.system_settings import SystemSettings
from src.models.user import User from src.models.user import User

View File

@@ -7,7 +7,7 @@ import os
from pathlib import Path from pathlib import Path
sys.path.insert(0, str(Path(__file__).parent.parent)) sys.path.insert(0, str(Path(__file__).parent.parent))
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from src.config.database import SessionLocal from src.shared.config.database import SessionLocal
from src.models.blog import BlogPost from src.models.blog import BlogPost
from src.models.user import User from src.models.user import User
from src.models.role import Role from src.models.role import Role

View File

@@ -3,7 +3,7 @@ import os
import json import json
sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'src')) sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'src'))
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from src.config.database import SessionLocal from src.shared.config.database import SessionLocal
from src.models.page_content import PageContent, PageType from src.models.page_content import PageContent, PageType
def seed_homepage_content(db: Session): def seed_homepage_content(db: Session):

View File

@@ -3,7 +3,7 @@ import os
from pathlib import Path from pathlib import Path
sys.path.insert(0, str(Path(__file__).parent)) sys.path.insert(0, str(Path(__file__).parent))
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from src.config.database import SessionLocal from src.shared.config.database import SessionLocal
from src.models.role import Role from src.models.role import Role
from src.models.room_type import RoomType from src.models.room_type import RoomType
from src.models.user import User from src.models.user import User

View File

@@ -3,7 +3,7 @@ import os
from pathlib import Path from pathlib import Path
sys.path.insert(0, str(Path(__file__).parent.parent)) sys.path.insert(0, str(Path(__file__).parent.parent))
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from src.config.database import SessionLocal from src.shared.config.database import SessionLocal
from src.models.loyalty_reward import LoyaltyReward, RewardType from src.models.loyalty_reward import LoyaltyReward, RewardType
from src.models.loyalty_tier import LoyaltyTier, TierLevel from src.models.loyalty_tier import LoyaltyTier, TierLevel
from datetime import datetime, timedelta from datetime import datetime, timedelta

View File

@@ -3,7 +3,7 @@ import os
import json import json
sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'src')) sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'src'))
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from src.config.database import SessionLocal, engine from src.shared.config.database import SessionLocal, engine
from src.models.page_content import PageContent from src.models.page_content import PageContent
from src.models.user import User from src.models.user import User

View File

@@ -8,7 +8,7 @@ import json
sys.path.insert(0, str(Path(__file__).parent)) sys.path.insert(0, str(Path(__file__).parent))
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from src.config.database import SessionLocal from src.shared.config.database import SessionLocal
from src.models.page_content import PageContent, PageType from src.models.page_content import PageContent, PageType
from datetime import datetime from datetime import datetime

View File

@@ -3,7 +3,7 @@ import os
from pathlib import Path from pathlib import Path
sys.path.insert(0, str(Path(__file__).parent)) sys.path.insert(0, str(Path(__file__).parent))
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from src.config.database import SessionLocal, engine from src.shared.config.database import SessionLocal, engine
from src.models.room import Room, RoomStatus from src.models.room import Room, RoomStatus
from src.models.room_type import RoomType from src.models.room_type import RoomType
from datetime import datetime from datetime import datetime

View File

@@ -3,7 +3,7 @@ import os
from pathlib import Path from pathlib import Path
sys.path.insert(0, str(Path(__file__).parent)) sys.path.insert(0, str(Path(__file__).parent))
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from src.config.database import SessionLocal from src.shared.config.database import SessionLocal
from src.models.role import Role from src.models.role import Role
from src.models.user import User from src.models.user import User
import bcrypt import bcrypt

View File

View File

View 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'),
)

View File

@@ -2,7 +2,7 @@ from sqlalchemy import Column, Integer, String, Text, ForeignKey, DateTime, Enum
from sqlalchemy.orm import relationship from sqlalchemy.orm import relationship
from datetime import datetime from datetime import datetime
import enum import enum
from ..config.database import Base from ...shared.config.database import Base
class ChatStatus(str, enum.Enum): class ChatStatus(str, enum.Enum):
pending = 'pending' pending = 'pending'

View File

View 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))

View File

View File

View 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

View 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

View 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

View 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

View 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

View 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

View File

View File

View File

@@ -1,7 +1,7 @@
from sqlalchemy import Column, Integer, String, Text, DateTime, ForeignKey, JSON from sqlalchemy import Column, Integer, String, Text, DateTime, ForeignKey, JSON
from sqlalchemy.orm import relationship from sqlalchemy.orm import relationship
from datetime import datetime from datetime import datetime
from ..config.database import Base from ...shared.config.database import Base
class AuditLog(Base): class AuditLog(Base):
__tablename__ = 'audit_logs' __tablename__ = 'audit_logs'

View File

View File

@@ -1,9 +1,9 @@
from fastapi import APIRouter, Depends, HTTPException, Query from fastapi import APIRouter, Depends, HTTPException, Query
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from typing import Optional from typing import Optional
from ..config.database import get_db from ...shared.config.database import get_db
from ..middleware.auth import authorize_roles, get_current_user from ...security.middleware.auth import authorize_roles, get_current_user
from ..models.user import User from ...auth.models.user import User
from ..services.analytics_service import AnalyticsService from ..services.analytics_service import AnalyticsService
router = APIRouter(prefix='/analytics', tags=['analytics']) router = APIRouter(prefix='/analytics', tags=['analytics'])

View File

@@ -3,9 +3,9 @@ from sqlalchemy.orm import Session
from sqlalchemy import desc, or_, func from sqlalchemy import desc, or_, func
from typing import Optional from typing import Optional
from datetime import datetime from datetime import datetime
from ..config.database import get_db from ...shared.config.database import get_db
from ..middleware.auth import get_current_user, authorize_roles from ...security.middleware.auth import get_current_user, authorize_roles
from ..models.user import User from ...auth.models.user import User
from ..models.audit_log import AuditLog from ..models.audit_log import AuditLog
router = APIRouter(prefix='/audit-logs', tags=['audit-logs']) router = APIRouter(prefix='/audit-logs', tags=['audit-logs'])

View File

@@ -3,15 +3,15 @@ from sqlalchemy.orm import Session, load_only, joinedload
from sqlalchemy import func, and_ from sqlalchemy import func, and_
from typing import Optional from typing import Optional
from datetime import datetime, timedelta from datetime import datetime, timedelta
from ..config.database import get_db from ...shared.config.database import get_db
from ..middleware.auth import get_current_user, authorize_roles from ...security.middleware.auth import get_current_user, authorize_roles
from ..models.user import User from ...auth.models.user import User
from ..models.booking import Booking, BookingStatus from ...bookings.models.booking import Booking, BookingStatus
from ..models.payment import Payment, PaymentStatus from ...payments.models.payment import Payment, PaymentStatus
from ..models.room import Room, RoomStatus from ...rooms.models.room import Room, RoomStatus
from ..models.service_usage import ServiceUsage from ...hotel_services.models.service_usage import ServiceUsage
from ..models.service import Service from ...hotel_services.models.service import Service
from ..utils.response_helpers import success_response from ...shared.utils.response_helpers import success_response
router = APIRouter(prefix='/reports', tags=['reports']) router = APIRouter(prefix='/reports', tags=['reports'])
@router.get('') @router.get('')

View File

@@ -2,15 +2,15 @@ from sqlalchemy.orm import Session, load_only
from sqlalchemy import func, and_, or_, case, extract, distinct from sqlalchemy import func, and_, or_, case, extract, distinct
from typing import Optional, Dict, List, Any from typing import Optional, Dict, List, Any
from datetime import datetime, timedelta, date from datetime import datetime, timedelta, date
from ..models.booking import Booking, BookingStatus from ...bookings.models.booking import Booking, BookingStatus
from ..models.payment import Payment, PaymentStatus, PaymentMethod from ...payments.models.payment import Payment, PaymentStatus, PaymentMethod
from ..models.room import Room, RoomStatus from ...rooms.models.room import Room, RoomStatus
from ..models.room_type import RoomType from ...rooms.models.room_type import RoomType
from ..models.user import User from ...auth.models.user import User
from ..models.service_usage import ServiceUsage from ...hotel_services.models.service_usage import ServiceUsage
from ..models.service import Service from ...hotel_services.models.service import Service
from ..models.review import Review, ReviewStatus from ...reviews.models.review import Review, ReviewStatus
from ..models.invoice import Invoice from ...payments.models.invoice import Invoice
import logging import logging
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -290,7 +290,7 @@ class AnalyticsService:
start_date = end_date - timedelta(days=30) start_date = end_date - timedelta(days=30)
# Get staff users # 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( staff_users = db.query(User).join(Role, User.role_id == Role.id).filter(
or_(Role.name == 'staff', Role.name == 'admin') or_(Role.name == 'staff', Role.name == 'admin')
).all() ).all()

View File

@@ -2,7 +2,7 @@ from sqlalchemy.orm import Session
from typing import Optional, Dict, Any from typing import Optional, Dict, Any
from datetime import datetime from datetime import datetime
from ..models.audit_log import AuditLog 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__) logger = get_logger(__name__)
class AuditService: class AuditService:

View File

View File

View File

@@ -1,7 +1,7 @@
from sqlalchemy import Column, Integer, String, DateTime, Boolean, ForeignKey from sqlalchemy import Column, Integer, String, DateTime, Boolean, ForeignKey
from sqlalchemy.orm import relationship from sqlalchemy.orm import relationship
from datetime import datetime from datetime import datetime
from ..config.database import Base from ...shared.config.database import Base
class PasswordResetToken(Base): class PasswordResetToken(Base):
__tablename__ = 'password_reset_tokens' __tablename__ = 'password_reset_tokens'

View File

@@ -1,7 +1,7 @@
from sqlalchemy import Column, Integer, String, DateTime, ForeignKey from sqlalchemy import Column, Integer, String, DateTime, ForeignKey
from sqlalchemy.orm import relationship from sqlalchemy.orm import relationship
from datetime import datetime from datetime import datetime
from ..config.database import Base from ...shared.config.database import Base
class RefreshToken(Base): class RefreshToken(Base):
__tablename__ = 'refresh_tokens' __tablename__ = 'refresh_tokens'

View File

@@ -1,7 +1,7 @@
from sqlalchemy import Column, Integer, String, DateTime from sqlalchemy import Column, Integer, String, DateTime
from sqlalchemy.orm import relationship from sqlalchemy.orm import relationship
from datetime import datetime from datetime import datetime
from ..config.database import Base from ...shared.config.database import Base
class Role(Base): class Role(Base):
__tablename__ = 'roles' __tablename__ = 'roles'

View File

@@ -1,7 +1,7 @@
from sqlalchemy import Column, Integer, String, Boolean, Text, ForeignKey, DateTime, Numeric from sqlalchemy import Column, Integer, String, Boolean, Text, ForeignKey, DateTime, Numeric
from sqlalchemy.orm import relationship from sqlalchemy.orm import relationship
from datetime import datetime from datetime import datetime
from ..config.database import Base from ...shared.config.database import Base
class User(Base): class User(Base):
__tablename__ = 'users' __tablename__ = 'users'

View File

View File

@@ -5,12 +5,12 @@ from pathlib import Path
import aiofiles import aiofiles
import uuid import uuid
import os import os
from ..config.database import get_db from ...shared.config.database import get_db
from ..services.auth_service import auth_service 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 ..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 ..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 import Limiter, _rate_limit_exceeded_handler
from slowapi.util import get_remote_address from slowapi.util import get_remote_address
from slowapi.errors import RateLimitExceeded from slowapi.errors import RateLimitExceeded
@@ -58,7 +58,7 @@ async def register(
try: try:
result = await auth_service.register(db=db, name=register_request.name, email=register_request.email, password=register_request.password, phone=register_request.phone) 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 max_age = 7 * 24 * 60 * 60 # 7 days for registration
# Use secure cookies in production (HTTPS required) # Use secure cookies in production (HTTPS required)
# Set access token in httpOnly cookie for security # Set access token in httpOnly cookie for security
@@ -144,7 +144,7 @@ async def login(
status='success' status='success'
) )
return {'status': 'success', 'requires_mfa': True, 'user_id': result['user_id']} 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 max_age = 7 * 24 * 60 * 60 if login_request.rememberMe else 1 * 24 * 60 * 60
# Use secure cookies in production (HTTPS required) # Use secure cookies in production (HTTPS required)
# Set access token in httpOnly cookie for security # 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') raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail='Refresh token not found')
try: try:
result = await auth_service.refresh_access_token(db, refreshToken) 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 # Set new access token in httpOnly cookie
# Use 'lax' in development for cross-origin support, 'strict' in production # Use 'lax' in development for cross-origin support, 'strict' in production
samesite_value = 'strict' if settings.is_production else 'lax' samesite_value = 'strict' if settings.is_production else 'lax'
@@ -250,7 +250,7 @@ async def logout(
await auth_service.logout(db, refreshToken) await auth_service.logout(db, refreshToken)
# Delete both access and refresh token cookies # 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 # Use 'lax' in development for cross-origin support, 'strict' in production
samesite_value = 'strict' if settings.is_production else 'lax' samesite_value = 'strict' if settings.is_production else 'lax'
response.delete_cookie(key='refreshToken', path='/', secure=settings.is_production, samesite=samesite_value) 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 status_code = status.HTTP_404_NOT_FOUND
raise HTTPException(status_code=status_code, detail=str(e)) raise HTTPException(status_code=status_code, detail=str(e))
from ..services.mfa_service import mfa_service from ..services.mfa_service import mfa_service
from ..config.settings import settings from ...shared.config.settings import settings
@router.get('/mfa/init') @router.get('/mfa/init')
async def init_mfa(current_user: User=Depends(get_current_user), db: Session=Depends(get_db)): 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)): async def upload_avatar(request: Request, image: UploadFile=File(...), current_user: User=Depends(get_current_user), db: Session=Depends(get_db)):
try: try:
# Use comprehensive file validation (magic bytes + size) # 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 max_avatar_size = 2 * 1024 * 1024 # 2MB for avatars
# Validate file completely (MIME type, size, magic bytes, integrity) # Validate file completely (MIME type, size, magic bytes, integrity)

View File

@@ -3,14 +3,14 @@ from sqlalchemy.orm import Session
from sqlalchemy import or_ from sqlalchemy import or_
from typing import Optional from typing import Optional
import bcrypt import bcrypt
from ..config.database import get_db from ...shared.config.database import get_db
from ..middleware.auth import get_current_user, authorize_roles from ...security.middleware.auth import get_current_user, authorize_roles
from ..models.user import User from ..models.user import User
from ..models.role import Role from ..models.role import Role
from ..models.booking import Booking, BookingStatus from ...bookings.models.booking import Booking, BookingStatus
from ..utils.role_helpers import can_manage_users from ...shared.utils.role_helpers import can_manage_users
from ..utils.response_helpers import success_response from ...shared.utils.response_helpers import success_response
from ..services.audit_service import audit_service from ...analytics.services.audit_service import audit_service
from ..schemas.user import CreateUserRequest, UpdateUserRequest from ..schemas.user import CreateUserRequest, UpdateUserRequest
router = APIRouter(prefix='/users', tags=['users']) router = APIRouter(prefix='/users', tags=['users'])

View File

View File

View File

@@ -11,13 +11,13 @@ from ..models.user import User
from ..models.refresh_token import RefreshToken from ..models.refresh_token import RefreshToken
from ..models.password_reset_token import PasswordResetToken from ..models.password_reset_token import PasswordResetToken
from ..models.role import Role from ..models.role import Role
from ..utils.mailer import send_email from ...shared.utils.mailer import send_email
from ..utils.email_templates import ( from ...shared.utils.email_templates import (
welcome_email_template, welcome_email_template,
password_reset_email_template, password_reset_email_template,
password_changed_email_template password_changed_email_template
) )
from ..config.settings import settings from ...shared.config.settings import settings
import os import os
logger = logging.getLogger(__name__) 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: async def register(self, db: Session, name: str, email: str, password: str, phone: Optional[str] = None) -> dict:
# Validate password strength # 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) is_valid, errors = validate_password_strength(password)
if not is_valid: if not is_valid:
error_message = 'Password does not meet requirements: ' + '; '.join(errors) error_message = 'Password does not meet requirements: ' + '; '.join(errors)
@@ -361,7 +361,7 @@ class AuthService:
raise ValueError("Current password is incorrect") raise ValueError("Current password is incorrect")
# Validate new password strength # 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) is_valid, errors = validate_password_strength(password)
if not is_valid: if not is_valid:
error_message = 'New password does not meet requirements: ' + '; '.join(errors) error_message = 'New password does not meet requirements: ' + '; '.join(errors)

View File

@@ -6,9 +6,9 @@ import secrets
from urllib.parse import urlencode from urllib.parse import urlencode
import logging import logging
from ..models.security_event import OAuthProvider, OAuthToken from ...security.models.security_event import OAuthProvider, OAuthToken
from ..models.user import User from ..models.user import User
from ..config.logging_config import get_logger from ...shared.config.logging_config import get_logger
logger = get_logger(__name__) logger = get_logger(__name__)

View File

View File

View File

@@ -2,7 +2,7 @@ from sqlalchemy import Column, Integer, String, DateTime, Numeric, Boolean, Text
from sqlalchemy.orm import relationship from sqlalchemy.orm import relationship
from datetime import datetime from datetime import datetime
import enum import enum
from ..config.database import Base from ...shared.config.database import Base
class BookingStatus(str, enum.Enum): class BookingStatus(str, enum.Enum):
pending = 'pending' pending = 'pending'

View File

@@ -1,7 +1,7 @@
from sqlalchemy import Column, Integer, DateTime, Numeric, Text, ForeignKey from sqlalchemy import Column, Integer, DateTime, Numeric, Text, ForeignKey
from sqlalchemy.orm import relationship from sqlalchemy.orm import relationship
from datetime import datetime from datetime import datetime
from ..config.database import Base from ...shared.config.database import Base
class CheckInCheckOut(Base): class CheckInCheckOut(Base):
__tablename__ = 'checkin_checkout' __tablename__ = 'checkin_checkout'

View File

@@ -2,7 +2,7 @@ from sqlalchemy import Column, Integer, String, DateTime, Numeric, Boolean, Text
from sqlalchemy.orm import relationship from sqlalchemy.orm import relationship
from datetime import datetime from datetime import datetime
import enum import enum
from ..config.database import Base from ...shared.config.database import Base
class GroupBookingStatus(str, enum.Enum): class GroupBookingStatus(str, enum.Enum):
draft = 'draft' draft = 'draft'

View File

View File

@@ -5,25 +5,25 @@ from typing import Optional
from datetime import datetime from datetime import datetime
import random import random
import os import os
from ..config.database import get_db from ...shared.config.database import get_db
from ..config.settings import settings from ...shared.config.settings import settings
from ..middleware.auth import get_current_user, authorize_roles from ...security.middleware.auth import get_current_user, authorize_roles
from ..models.user import User from ...auth.models.user import User
from ..models.role import Role from ...auth.models.role import Role
from ..models.booking import Booking, BookingStatus from ..models.booking import Booking, BookingStatus
from ..models.room import Room, RoomStatus from ...rooms.models.room import Room, RoomStatus
from ..models.room_type import RoomType from ...rooms.models.room_type import RoomType
from ..models.payment import Payment, PaymentMethod, PaymentType, PaymentStatus from ...payments.models.payment import Payment, PaymentMethod, PaymentType, PaymentStatus
from ..models.service_usage import ServiceUsage from ...hotel_services.models.service_usage import ServiceUsage
from ..models.user_loyalty import UserLoyalty from ...loyalty.models.user_loyalty import UserLoyalty
from ..models.referral import Referral, ReferralStatus from ...loyalty.models.referral import Referral, ReferralStatus
from ..services.room_service import normalize_images, get_base_url from ...rooms.services.room_service import normalize_images, get_base_url
from fastapi import Request from fastapi import Request
from ..utils.mailer import send_email from ...shared.utils.mailer import send_email
from ..utils.email_templates import booking_confirmation_email_template, booking_status_changed_email_template from ...shared.utils.email_templates import booking_confirmation_email_template, booking_status_changed_email_template
from ..services.loyalty_service import LoyaltyService from ...loyalty.services.loyalty_service import LoyaltyService
from ..utils.currency_helpers import get_currency_symbol from ...shared.utils.currency_helpers import get_currency_symbol
from ..utils.response_helpers import success_response from ...shared.utils.response_helpers import success_response
from ..schemas.booking import CreateBookingRequest, UpdateBookingRequest from ..schemas.booking import CreateBookingRequest, UpdateBookingRequest
router = APIRouter(prefix='/bookings', tags=['bookings']) 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') raise HTTPException(status_code=409, detail='Room already booked for the selected dates')
# Check for maintenance blocks # 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( maintenance_block = db.query(RoomMaintenance).filter(
and_( and_(
RoomMaintenance.room_id == room_id, 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) # Calculate services total if any (using Pydantic model)
services_total = 0.0 services_total = 0.0
if services: if services:
from ..models.service import Service from ...hotel_services.models.service import Service
for service_item in services: for service_item in services:
service_id = service_item.service_id service_id = service_item.service_id
quantity = service_item.quantity quantity = service_item.quantity
@@ -314,8 +314,8 @@ async def create_booking(booking_data: CreateBookingRequest, current_user: User=
# Process referral code if provided # Process referral code if provided
if referral_code: if referral_code:
try: try:
from ..services.loyalty_service import LoyaltyService from ...loyalty.services.loyalty_service import LoyaltyService
from ..models.system_settings import SystemSettings from ...system.models.system_settings import SystemSettings
# Check if loyalty program is enabled # Check if loyalty program is enabled
setting = db.query(SystemSettings).filter( 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}") logger.warning(f"Failed to process referral code {referral_code}: {referral_error}")
# Don't fail the booking if referral processing fails # Don't fail the booking if referral processing fails
if payment_method in ['stripe', 'paypal']: 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': if payment_method == 'stripe':
payment_method_enum = PaymentMethod.stripe payment_method_enum = PaymentMethod.stripe
elif payment_method == 'paypal': elif payment_method == 'paypal':
@@ -352,14 +352,14 @@ async def create_booking(booking_data: CreateBookingRequest, current_user: User=
db.flush() 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)}') 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: 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) 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.add(deposit_payment)
db.flush() db.flush()
logger.info(f'Deposit payment created: ID={deposit_payment.id}, amount={deposit_amount}, percentage={deposit_percentage}%') 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) # Add service usages (services already extracted from Pydantic model)
if services: if services:
from ..models.service import Service from ...hotel_services.models.service import Service
for service_item in services: for service_item in services:
service_id = service_item.service_id service_id = service_item.service_id
quantity = service_item.quantity quantity = service_item.quantity
@@ -375,7 +375,7 @@ async def create_booking(booking_data: CreateBookingRequest, current_user: User=
# Send booking confirmation notification # Send booking confirmation notification
try: try:
from ..services.notification_service import NotificationService from ...notifications.services.notification_service import NotificationService
if booking.status == BookingStatus.confirmed: if booking.status == BookingStatus.confirmed:
NotificationService.send_booking_confirmation(db, booking) NotificationService.send_booking_confirmation(db, booking)
except Exception as e: 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}') logger.warning(f'Failed to send booking confirmation notification: {e}')
try: try:
from ..services.invoice_service import InvoiceService from ...payments.services.invoice_service import InvoiceService
from ..utils.mailer import send_email from ...shared.utils.mailer import send_email
from sqlalchemy.orm import joinedload, selectinload from sqlalchemy.orm import joinedload, selectinload
booking = db.query(Booking).options(selectinload(Booking.service_usages).selectinload(ServiceUsage.service)).filter(Booking.id == booking.id).first() 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 = {} company_settings = {}
for key in ['company_name', 'company_address', 'company_phone', 'company_email', 'company_tax_id', 'company_logo_url']: 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() 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() 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: if not booking:
raise HTTPException(status_code=404, detail='Booking not found') 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: if not is_admin(current_user, db) and booking.user_id != current_user.id:
raise HTTPException(status_code=403, detail='Forbidden') raise HTTPException(status_code=403, detail='Forbidden')
import logging import logging
@@ -612,7 +612,7 @@ async def cancel_booking(id: int, current_user: User=Depends(get_current_user),
if not active_booking: if not active_booking:
# Check for maintenance # Check for maintenance
from ..models.room_maintenance import RoomMaintenance, MaintenanceStatus from ...rooms.models.room_maintenance import RoomMaintenance, MaintenanceStatus
active_maintenance = db.query(RoomMaintenance).filter( active_maintenance = db.query(RoomMaintenance).filter(
and_( and_(
RoomMaintenance.room_id == booking.room_id, 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.flush()
db.commit() db.commit()
try: 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_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') 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) 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) # Set room to cleaning when checked out (housekeeping needed)
if room: if room:
# Check if there's active maintenance # 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( active_maintenance = db.query(RoomMaintenance).filter(
and_( and_(
RoomMaintenance.room_id == room.id, RoomMaintenance.room_id == room.id,
@@ -710,7 +710,7 @@ async def update_booking(id: int, booking_data: UpdateBookingRequest, current_us
if not active_booking: if not active_booking:
# Check for maintenance # Check for maintenance
from ..models.room_maintenance import RoomMaintenance, MaintenanceStatus from ...rooms.models.room_maintenance import RoomMaintenance, MaintenanceStatus
active_maintenance = db.query(RoomMaintenance).filter( active_maintenance = db.query(RoomMaintenance).filter(
and_( and_(
RoomMaintenance.room_id == room.id, 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 # Send booking confirmation notification if status changed to confirmed
if new_status == BookingStatus.confirmed: if new_status == BookingStatus.confirmed:
try: try:
from ..services.notification_service import NotificationService from ...notifications.services.notification_service import NotificationService
NotificationService.send_booking_confirmation(db, booking) NotificationService.send_booking_confirmation(db, booking)
except Exception as e: except Exception as e:
import logging 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 status_value and old_status != booking.status:
if booking.status in [BookingStatus.confirmed, BookingStatus.cancelled]: if booking.status in [BookingStatus.confirmed, BookingStatus.cancelled]:
try: 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_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') 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: if booking.status == BookingStatus.confirmed:
@@ -797,7 +797,7 @@ async def update_booking(id: int, booking_data: UpdateBookingRequest, current_us
if booking.user: if booking.user:
try: try:
# Check if booking already earned points # 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( existing_points = db.query(LoyaltyPointTransaction).filter(
LoyaltyPointTransaction.booking_id == booking.id, LoyaltyPointTransaction.booking_id == booking.id,
LoyaltyPointTransaction.source == TransactionSource.booking 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() user_loyalty = db.query(UserLoyalty).filter(UserLoyalty.user_id == booking.user_id).first()
if user_loyalty and user_loyalty.referral_code: if user_loyalty and user_loyalty.referral_code:
# Check if there's a referral for this user that hasn't been rewarded yet # 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 = db.query(Referral).filter(
Referral.referred_user_id == booking.user_id, Referral.referred_user_id == booking.user_id,
Referral.booking_id == booking.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 = booking_data.get('services', [])
services_total = 0.0 services_total = 0.0
if services: if services:
from ..models.service import Service from ...hotel_services.models.service import Service
for service_item in services: for service_item in services:
service_id = service_item.get('service_id') service_id = service_item.get('service_id')
quantity = service_item.get('quantity', 1) 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() db.flush()
# Create payment records based on payment_status # 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 from datetime import datetime as dt
# Determine payment method enum # 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 # Add service usages if any
if services: if services:
from ..models.service import Service from ...hotel_services.models.service import Service
for service_item in services: for service_item in services:
service_id = service_item.get('service_id') service_id = service_item.get('service_id')
quantity = service_item.get('quantity', 1) quantity = service_item.get('quantity', 1)

View File

@@ -4,18 +4,18 @@ from typing import Optional, List
from datetime import datetime from datetime import datetime
from decimal import Decimal from decimal import Decimal
from ..config.database import get_db from ...shared.config.database import get_db
from ..middleware.auth import get_current_user, authorize_roles from ...security.middleware.auth import get_current_user, authorize_roles
from ..models.user import User from ...auth.models.user import User
from ..models.role import Role from ...auth.models.role import Role
from ..models.group_booking import ( from ..models.group_booking import (
GroupBooking, GroupBookingMember, GroupRoomBlock, GroupPayment, GroupBooking, GroupBookingMember, GroupRoomBlock, GroupPayment,
GroupBookingStatus, PaymentOption GroupBookingStatus, PaymentOption
) )
from ..models.room import Room from ...rooms.models.room import Room
from ..models.room_type import RoomType from ...rooms.models.room_type import RoomType
from ..services.group_booking_service import GroupBookingService 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 from fastapi import Request
router = APIRouter(prefix='/group-bookings', tags=['group-bookings']) router = APIRouter(prefix='/group-bookings', tags=['group-bookings'])

View File

View File

@@ -9,10 +9,10 @@ from ..models.group_booking import (
GroupBookingStatus, PaymentOption GroupBookingStatus, PaymentOption
) )
from ..models.booking import Booking, BookingStatus from ..models.booking import Booking, BookingStatus
from ..models.room import Room, RoomStatus from ...rooms.models.room import Room, RoomStatus
from ..models.room_type import RoomType from ...rooms.models.room_type import RoomType
from ..models.user import User from ...auth.models.user import User
from ..models.payment import Payment, PaymentStatus, PaymentMethod from ...payments.models.payment import Payment, PaymentStatus, PaymentMethod
import logging import logging
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)

Some files were not shown because too many files have changed in this diff Show More